0

I have a Controller Action like this:

[HttpPost("Post")]
public async Task Post([FromBody] UpdateDataCommand command)
{
    await _mediator.Send(command);
}

It is done in .Net Core, and is using MediatR to process commands.

Now, the UpdateDataCommand has a integer StationId property that identifies the Station number. When a client application calls this method by doing a Post, it updates data in the database. What I want to do using Rx .Net is to somehow start a timer after the Await _mediator.Send(command). The timer will be set to 1 minute. After 1 minute, I want to call another method that will set the flag in the database but only for this StationId. If someone does a Post using the same StationId, the timer should reset itself.

In pseudo-code looks like this:

[HttpPost("Post")]
public async Task Post([FromBody] UpdateDataCommand command)
{
    int stationId = command.StationId;
    // let's assume stationId==2

    //saves data for stationId==2
    await _mediator.Send(command);

    //Start a timer of 1 min
    //if timer fires (meaning the 1 minute has passed) call Method2();
    //if client does another "Post" for stationId==2 in the meantime 
      (let's say that the client does another "Post" for stationId==2 after 20 sec)
      then reset the timer
}

How to do this using Reactive Extensions in.Net?

UPDATE (@Enigmativity): It still doesn't work,I put the timer to 10sec and if you look at the output times you'll see that I have made a Post on 09:17:49 (which started a timer of 10 sec), then I made a new Post at 09:17:55 (which has started another timer, but it should only have reset the old one) and bothe the timers kicked off, one 10 secs after the first call, and another 10 sec after the second call.: Application output

Luka
  • 4,075
  • 3
  • 35
  • 61

2 Answers2

1

I haven't been able to test this, but I think this is pretty close:

private Subject<UpdateDataCommand> posted = new Subject<UpdateDataCommand>();

private void PostInitialize()
{
    posted
        .GroupBy(x => x.StationId)
        .Select(gxs =>
            gxs
                .Select(x =>
                    Observable
                        .Timer(TimeSpan.FromMinutes(1.0))
                        .Select(_ => x))
                .Switch())
        .Merge()
        .Subscribe(stationId =>
        {
            /* update database */
        });
}

public async Task Post(UpdateDataCommand command)
{
    int stationId = command.StationId;
    await _mediator.Send(command);
    posted.OnNext(command);
}

Let me know if this gets it close.

You have to call PostInitialize to set it up before your start posting update data commands.


Here's a test that shows that this works:

var rnd = new Random();

var posted =
    Observable
        .Generate(
            0, x => x < 20, x => x + 1, x => x,
            x => TimeSpan.FromSeconds(rnd.NextDouble()));

posted
    .GroupBy(x => x % 3)
    .Select(gxs =>
        gxs
            .Select(x =>
                Observable
                    .Timer(TimeSpan.FromSeconds(1.9))
                    .Select(_ => x))
            .Switch())
    .Merge()
    .Subscribe(x => Console.WriteLine(x));

I get results like:

3
4
14
15
17
18
19

Since I've used .GroupBy(x => x % 3) this will always output 17, 18, & 19 - but will output earlier numbers if the random interval is sufficiently large.

Enigmativity
  • 113,464
  • 11
  • 89
  • 172
  • I tried this solution but it didn't work as expected. It does not reset when the client does another Post. Ie.... at time 05:00:20h client does a Post for stationId:1. The application starts a timer of 1 minute. At 05:00:35h the client doeas another Post for the same stationId of 1. The first timer is not reset and it kicks off at 05:01:20h and the timer of the second Post kicks too at 05:01:25h. – Luka Jun 18 '19 at 12:02
  • @Luka - Any chance you could provide a [mcve] with sample source data that I could use to get the right asnwer? – Enigmativity Jun 18 '19 at 12:13
  • @Luka Just use Throttle instead of Sample. Then it should meet your requirements. – Felix Keil Jun 18 '19 at 14:38
  • I changed Sample with Throttle but the same happens. – Luka Jun 18 '19 at 19:36
  • @Luka - Can you post a [mcve] in your question? Your app isn't minimal and it isn't something that I can just hit run on. – Enigmativity Jun 19 '19 at 00:11
  • @Luka - I had an aha moment - try my updated answer. – Enigmativity Jun 19 '19 at 00:13
  • See my update, it still doesn't work. The sample application I posted on gitlab, is the minimum application for the scenario I need. It is a working .net core 3.0 Webapi project that starts a server and waits for a POST. I use Postman to do the Post. – Luka Jun 19 '19 at 07:30
  • The code in your test works because it is a single thread, but in asp each Post is another thread. I have created a new sample application for you to test here: https://gitlab.com/l.cetina/test-console It's a console application that emulates multi-threaded environment of an asp.net Post. – Luka Jun 19 '19 at 07:59
  • @Luka - You've marked **itminus**'s answer as correct. Surely it has the same threading issue? – Enigmativity Jun 19 '19 at 09:13
  • @Luka - You can't create a new `Test` instance each time. – Enigmativity Jun 19 '19 at 09:16
  • Itminus solution works because he is manually managing subscriptions and is keeping them in a concurrent (thread safe) collection. While yours is using purely Rx style by grouping them. – Luka Jun 19 '19 at 09:40
  • Yes, I can, because every Post in asp.net is a new request, that is creating a new controller instance. So by creating new Test instance i emulate the creation of new controller instance. – Luka Jun 19 '19 at 09:41
  • @Luka - "because he is manually managing subscriptions and is keeping them in a concurrent (thread safe) collection. While yours is using purely Rx style by grouping them" - that doesn't make sense to me. My solution doesn't require a concurrent (thread safe) collection. And his is store multiple queries - mine only has one. His solution must persist through multiple calls and so mine must be able to too. – Enigmativity Jun 19 '19 at 11:54
  • @Luka - If you're saying that a new controller is created then **itminus**'s solution would also lose the instance of `SubscriptionManager`. I don't understand how the solutions differ in that regard? – Enigmativity Jun 19 '19 at 11:56
  • Because SubscriptionManager is a signleton instance injected by DI container, So everytime a controller is created DI container injects the same SubscriptionManager instance. – Luka Jun 19 '19 at 12:03
  • @Luka - So do the same with `Test`. – Enigmativity Jun 19 '19 at 12:06
  • Oh it would work if I could make Controllers to be signleton.. If I need to do an intermediary class to support this, than this is the same solution as Itminus. – Luka Jun 21 '19 at 10:03
  • @Luka - Except there is a limited race condition and no need for extra state. And, if you had provided the complete code, I could have reduced it all down to a single query. – Enigmativity Jun 21 '19 at 11:10
0

To start a timer using Rx.Net, we could invoke:

var subscription = Observable.Timer(TimeSpan.FromSeconds(timeout))
    .Subscribe(
        value =>{    /* ... */ }
    );

To cancel this subscription, we just need dispose this subscription later:

subscription.Dispose();

The problem is how to persist the subscription. One approach is to create a SubscriptionManager service (singleton) , thus we can invoke such a service to schedule a task and then cancel it later within the controller action as below:

// you controller class

    private readonly ILogger<HomeController> _logger;       // injected by DI
    private readonly SubscriptionManager _subscriptionMgr;  // injected by DI


    public async Task Post(...)
    {
        ...
        // saves data for #stationId
        // Start a timer of 1 min
        this._subscriptionMgr.ScheduleForStationId(stationId);    // schedule a task that for #stationId that will be executed in 60s
    }


    [HttpPost("/Command2")]
    public async Task Command2(...)
    {
        int stationId =  command.StationId;
        if( shouldCancel ){
            this._subscriptionMgr.CancelForStationId(stationId);  // cancel previous task for #stationId
        }
    }

If you like to manage the subscriptions within memory, we can use a ConcurrentDictionary to store the subscirptions:

public class SubscriptionManager : IDisposable
{
    private ConcurrentDictionary<string,IDisposable> _dict;
    private readonly IServiceProvider _sp;
    private readonly ILogger<SubscriptionManager> _logger;

    public SubscriptionManager(IServiceProvider sp, ILogger<SubscriptionManager> logger)
    {
        this._dict= new ConcurrentDictionary<string,IDisposable>();
        this._sp = sp;
        this._logger = logger;
    }

    public IDisposable ScheduleForStationId(int stationId)
    {
        var timeout = 60;
        this._logger.LogWarning($"Task for Station#{stationId} will be exexuted in {timeout}s") ;
        var subscription = Observable.Timer(TimeSpan.FromSeconds(timeout))
            .Subscribe(
                value =>{  
                    // if you need update the db, create a new scope:
                    using(var scope = this._sp.CreateScope()){
                        var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
                        var station=dbContext.StationStatus.Where(ss => ss.StationId == stationId)
                            .FirstOrDefault();
                        station.Note = "updated";
                        dbContext.SaveChanges();
                    }
                    this._logger.LogWarning($"Task for Station#{stationId} has been executed") ;
                },
                e =>{
                    Console.WriteLine("Error!"+ e.Message);
                }
            );
        this._dict.AddOrUpdate( stationId.ToString(), subscription , (id , sub)=> {
            sub.Dispose();       // dispose the old one
            return subscription;
        });
        return subscription;
    }

    public void CancelForStationId(int stationId)
    {
        IDisposable subscription = null;
        this._dict.TryGetValue(stationId.ToString(), out subscription);
        this._logger.LogWarning($"Task for station#{stationId} has been canceled");
        subscription?.Dispose();

        // ... if you want to update the db , create a new scope
        using(var scope = this._sp.CreateScope()){
            var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
            var station=dbContext.StationStatus.Where(ss => ss.StationId == stationId)
                .FirstOrDefault();
            station.Note = "canceled";
            dbContext.SaveChanges();
            this._logger.LogWarning("The db has been changed");
        }
    }

    public void Dispose()
    {
        foreach(var entry in this._dict){
            entry.Value.Dispose();
        }
    }
}

Another Approach is to create a flat record to a task manager (like cron), but it won't use Rx.NET at all.

itminus
  • 23,772
  • 2
  • 53
  • 88
  • This is what I was looking for. This works without problems. Thanks for your time. – Luka Jun 18 '19 at 11:58
  • Just a warning on this - it's a very "un-Rx" way of doing this. There's a lot of external state required - state should be maintain solely within queries - and there's a good chance for race conditions. – Enigmativity Jun 19 '19 at 03:01
  • @Enigmativity As you remind previously, I'm now realizing there's will be some problems with the `AddOrUpdate()` (not full atomic) . I like your answer. But the problem is how to maintain a state that will be shared between different requests. A controller is actually not singleton, the way you define a `posted` field to share the state seems not working? – itminus Jun 19 '19 at 03:08
  • @itminus - My use of a subject is a hack because the OP didn't give me enough code to make a single query. I wouldn't normally use it. But it should be working. – Enigmativity Jun 19 '19 at 03:27
  • @Enigmativity I'll test it later. If it works for me, I'll come back and suggest the OP should accept your answer :) – itminus Jun 19 '19 at 04:35
  • @Enigmativity Hi, I tried your code but still cannot make it work. Could you please elaborate on how your `posted` field is shared by two requests ? I see there's a `PostInitialize()` , should I put it into the constructor of controller ? – itminus Jun 19 '19 at 05:51
  • @itminus - Yes, the `PostInitialize()` code must be run before this will work. Again, because the OP didn't give me enough code to wrap the whole thing in a query we have to use the subject and hence the `PostInitialize()` needs to be run. – Enigmativity Jun 19 '19 at 05:54
  • @Enigmativity I'm still confused: the Controller is actually a "scoped" service, it will be created for every request. Every time the client sends a request, there's will be a brand new controller instance and also a brand new `posted` field. Does that matter? – itminus Jun 19 '19 at 06:10
  • @itminus - Yes, that matters. There should only ever be a single call to `PostInitialize()` and only a single instance of `Subject posted`. – Enigmativity Jun 19 '19 at 06:17
  • @Enigmativity Does that mean I need share the state (i.e the `posted` singleton) between two different requests? – itminus Jun 19 '19 at 06:21
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/195169/discussion-between-itminus-and-enigmativity). – itminus Jun 19 '19 at 06:21