简体   繁体   English

Cron 可观察序列

[英]Cron Observable Sequence

I would like to create an observable sequence using reactive extensions (RX) and NCrontab.我想使用反应式扩展 (RX) 和 NCrontab 创建一个可观察序列。 The sequence would differ from something like Observable.Timer() in that the period and due time are not fixed.该序列将不同于Observable.Timer()类的东西,因为周期和到期时间不是固定的。 After reading this article it seems that Observable.Generate() is the way to go.阅读这篇文章后,似乎Observable.Generate()是要走的路。 I am thinking of two variants: one which runs within bounds and one that runs forever.我正在考虑两种变体:一种在界限内运行,一种永远运行。 Do these implementation make sense?这些实现有意义吗?

public static IObservable<DateTime> Cron(string cron)
{
    var schedule = CrontabSchedule.Parse(cron);
    return Observable.Generate(DateTime.Now, d=>true, d => DateTime.Now, d => d,
        d => new DateTimeOffset(schedule.GetNextOccurrence(d)));
}

public static IObservable<DateTime> Cron(string cron, DateTime start, DateTime end)
{
    var schedule = CrontabSchedule.Parse(cron);
    return Observable.Generate(start, d => d < end, d => DateTime.Now, d => d,
        d => new DateTimeOffset(schedule.GetNextOccurrence(d)));
}

update : These seem to work empirically, however I added an overload which takes an IScheduler and cannot seem to get the sequence to trigger in a unit test.更新:这些似乎凭经验工作,但是我添加了一个重载,它需要一个IScheduler并且似乎无法在单元测试中触发序列。 Am I using TestScheduler wrong or is there an issue with the function implementation?我使用TestScheduler错误还是函数实现有问题?

public static IObservable<int> Cron(string cron, IScheduler scheduler)
{
    var schedule = CrontabSchedule.Parse(cron);
    return Observable.Generate(0, d => true, d => d + 1, d => d,
        d => new DateTimeOffset(schedule.GetNextOccurrence(scheduler.Now.DateTime)), scheduler);
}

[TestClass]
public class EngineTests
{
    [TestMethod]
    public void TestCron()
    {
        var scheduler = new TestScheduler();
        var cron = "* * * * *";
        var values = new List<int>();
        var disp = ObservableCron.Cron(cron, scheduler).Subscribe(values.Add);
        scheduler.AdvanceBy(TimeSpan.TicksPerMinute - 1);
        scheduler.AdvanceBy(1);
        scheduler.AdvanceBy(1);
        Assert.IsTrue(values.Count> 0);
    }
}

It looks like a combination of issues.看起来像是问题的组合。 First, the Observable.Generate overload that I am using takes a Func<int,DateTimeOffset> parameter to determine the next time to trigger.首先,我使用的Observable.Generate重载采用Func<int,DateTimeOffset>参数来确定下一次触发的时间。 I was passing in a new DateTimeOffset based on the local time of the scheduler rather than Utc, which was causing the new 'DateTimeOffset` to shift.我正在根据调度程序的本地时间而不是 Utc 传入一个新的DateTimeOffset ,这导致新的“DateTimeOffset”发生变化。 See this question for an explanation.请参阅此问题以获取解释。 The correct function is below:正确的函数如下:

public static IObservable<int> Cron(string cron, IScheduler scheduler)
{
    var schedule = CrontabSchedule.Parse(cron);
    return Observable.Generate(0, d => true, d => d + 1, d => d,
        d => new DateTimeOffset(schedule.GetNextOccurrence(scheduler.Now.UtcDateTime)), scheduler);
}

As far as testing goes, I came up with something that demonstrates the intent a little better:就测试而言,我想出了一些可以更好地展示意图的东西:

[TestMethod]
public void TestCronInterval()
{
    var scheduler = new TestScheduler();
    var end = scheduler.Now.UtcDateTime.AddMinutes(10);
    const string cron = "*/5 * * * *";
    var i = 0;
    var seconds = 0;
    var sub = ObservableCron.Cron(cron, scheduler).Subscribe(x => i++);
    while (i < 2)
    {
        seconds++;
        scheduler.AdvanceBy(TimeSpan.TicksPerSecond);
    }
    Assert.IsTrue(seconds == 600);
    Assert.AreEqual(end, scheduler.Now.UtcDateTime);
    sub.Dispose();
}

I have used this solution without schedulers using Cronos :我使用了这个没有使用Cronos调度程序的解决方案:

public static IObservable<DateTimeOffset> ToObservable(this ICronScheduleObservableConfiguration configuration)
{
    Validate(configuration);
    var schedule = configuration.Expression;
    DateTimeOffset? next = null;
    return Observable.Generate(
        DateTimeOffset.Now,
            i => true,
            i => (next = schedule.GetNextOccurrence(i, configuration.TimeZone)) ?? DateTimeOffset.Now,
            i => next,
            i => i
        ).
        Where(i => i.HasValue).
        Select(i => i.Value);
}

public interface ICronScheduleObservableConfiguration :
    IObservableConfiguration
{
    /// <summary>
    /// Cron schedule with format: https://github.com/HangfireIO/Cronos#cron-format
    /// </summary>
    /// <value>Non-empty</value>
    string Schedule { get; }
    /// <summary>
    /// <see cref="Schedule"/> format
    /// </summary>
    CronFormat Format { get; }
    /// <summary>
    /// Parsed <see cref="Schedule"/> using <see cref="Format"/>
    /// </summary>
    /// <value>non-null</value>
    /// <exception cref="CronFormatException">Parsing with <see cref="CronExpression.Parse(string, CronFormat)"/> failed</exception>
    CronExpression Expression { get; }
    /// <summary>
    /// Time zone used for computing schedule times with <see cref="CronExpression.GetNextOccurrence(DateTimeOffset, TimeZoneInfo, bool)"/>
    /// </summary>
    /// <value>non-null</value>
    TimeZoneInfo TimeZone { get; }
}

First, scheduler.Now.DateTime is not going to give you real times in your unit test with a TestScheduler .首先, scheduler.Now.DateTime不会在您的单元测试中使用TestScheduler为您提供实时时间。 It is going to give you virtual times based upon some pre-defined start time.它会根据一些预定义的开始时间为您提供虚拟时间。 You should probably use AdvanceTo to initialize the clock to something that corresponds with your crontab test data.您可能应该使用AdvanceTo将时钟初始化为与您的 crontab 测试数据相对应的内容。

For this example test, that is probably not your problem.对于此示例测试,这可能不是您的问题。 Your problem is most likely that you are writing your test at the granularity of a Tick .您的问题很可能是您正在以Tick的粒度编写测试。 That rarely works.这很少奏效。 Because, for the TestScheduler , when a scheduled action occurs on Tick t that schedules another action to execute "immediately", that next action won't actually execute until Tick t+1 .因为,对于TestScheduler ,当在 Tick t上发生安排另一个动作“立即”执行的预定动作时,下一个动作实际上不会执行,直到 Tick t+1 If that action schedules another action, then it won't execute until tick t+2 , etc. So, unless you fully understand exactly how Generate schedules its work, you want to avoid writing tick-level tests.如果该操作调度另一个操作,则它不会执行直到滴答t+2等。因此,除非您完全了解Generate如何安排其工作,否则您希望避免编写滴答级别的测试。

Instead try to test at the granularity that the code you are testing supports.而是尝试以您正在测试的代码支持的粒度进行测试。 In this case, I think that is minutes...so write your test to advance 59 seconds, see that nothing happened, then advance 2 more seconds and see if you got what you expected.在这种情况下,我认为那是几分钟……所以写你的测试来推进 59 秒,看看什么也没发生,然后再推进 2 秒,看看你是否达到了你的预期。 Or, better yet, use the TestScheduler.CreateObserver method and just advance once或者,更好的是,使用TestScheduler.CreateObserver方法并只前进一次

var scheduler = new TestScheduler();

// set the virtual clock to something that corresponds with my test crontab data
scheduler.AdvanceTo(someDateTimeOffset);

// now your tests...
var cron = "* * * * *";
var observer = scheduler.CreateObserver();

var disp = ObservableCron.Cron(cron, scheduler).Subscribe(observer);

// advance the clock by 61 seconds
scheduler.AdvanceBy(TimeSpan.FromSeconds(61).Ticks);

// check the contents of the observer
Assert.IsTrue(observer.Messages.Count > 0);

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM