Scheduler built with observables v2 (follow-up)

The name of the pictureThe name of the pictureThe name of the pictureClash Royale CLAN TAG#URR8PPP





.everyoneloves__top-leaderboard:empty,.everyoneloves__mid-leaderboard:empty margin-bottom:0;







up vote
3
down vote

favorite












It would probably be too easy if the old Scheduler worked as intended being that simple. But an eye-openig review showed that from time to time it'll miss one second due to 14ms timer incaccuracy. This of course would have bad implications on the schedules (they wouldn't fire if their second was skipped).




I spent sometime experimenting with various solutions to fix the missing second bug and I think I found a solution that works nicely. I didn't want it to be too complex, I'm not (currently) writing any systems essential for life. It just should make sure nothing is missing. Without it the observable will accumulate all published values and the first subscriber will recieve them all. We don't want this to happen because we are interested at what's now and now in the history.




So how does the Scheduler work? I turned it into a hot observable with Publish + Connect. I want it to be ticking regardles if there is anyone listening.



The new Scheduler also can check how many jobs are already running and decline starting a new one.



How did I fix the missing second issue? The Scheduler is publishing timestamps the same way it did before but now it additionally checks if the difference between the last and the current timestamp is greater then 1s. If it is, it publishes the missing second before publishing the current one. This is achieved by switching from Select to SelectMany. Two intervals published virtually at the same moment don't really matter. More important is to guarantee that nothing is left out and that jobs at any second can be triggered. I think it's extremely unlikely that this would cause any issues ever.



public class Scheduler : IDisposable

public const int UnlimitedJobParallelism = -1;

private const int ZeroJobCounter = 0;

private IConnectableObservable<DateTime> _scheduler;

private IDisposable _disconnect;

private readonly ConcurrentDictionary<Job, int> _jobCounters;

public Scheduler(TimeSpan period, IDateTime dateTime)

var lastTimestamp = dateTime.Now();
_scheduler =
Observable
.Interval(period)
.SelectMany(_ =>

// If we missed one second due to time inaccuracy,
// this makes sure to publish the missing second too
// so that all jobs at that second can also be triggered.
var now = dateTime.Now();
var timestampDifference = now.Second - lastTimestamp.Second;
var timestamps =
Enumerable
// When starting the scheduler it might occur
// that the difference is a negative number (-59).
// If this is the case, make it 1.
.Range(1, timestampDifference < 0 ? 1 : timestampDifference)
.Select(second => lastTimestamp.AddSeconds(second));
lastTimestamp = now;
return timestamps.Dump();
)
// Share the actual data values and not just the observable instance.
.Publish();

// Turn this into a hot observable and start ticking
// regardless if there is anything listening.
_disconnect = _scheduler.Connect();
_jobCounters = new ConcurrentDictionary<Job, int>();


public Scheduler()
: this(TimeSpan.FromSeconds(1), new LocalDateTime())


public IDisposable Schedule(Job job)

var cronExpression = CronExpression.Parse(job.CronExpression);
var unschedule =
_scheduler
.Where(cronExpression.Contains)
.Subscribe(async timestamp =>

if (CanExecute(job))

_jobCounters.AddOrUpdate(job, 1, (j, c) => c + 1);
await job
.ExecuteAsync(timestamp)
.ContinueWith(_ =>
_jobCounters
.AddOrUpdate(job, 1, (j, c) => c - 1)
);

else

$"'job.Name' is alredy running!".Dump();

);

return Disposable.Create(() =>

_jobCounters.TryRemove(job, out var _);
unschedule.Dispose();
);


private bool CanExecute(Job job)

// Reversed the second condition to make the left side consistent.
return
job.MaxDegreeOfParallelism == UnlimitedJobParallelism

public void Dispose()

// Stop ticking.
_disconnect.Dispose();





There are also two new helpers. An extensions for the scheduler and the Job class.



public static class SchedulerExtensions

public static IDisposable Schedule(this Scheduler scheduler, string cronExpression, Func<DateTime, Task> execute, int maxDegreeOfParallelism = 1)

return scheduler.Schedule(new Job

CronExpression = cronExpression,
ExecuteAsync = execute,
MaxDegreeOfParallelism = maxDegreeOfParallelism
);



public class Job

public string Name get; set;

public string CronExpression get; set;

public Func<DateTime, Task> ExecuteAsync get; set;

public int MaxDegreeOfParallelism get; set; = 1;




Example



The usage didn't change much, just the signatures.



void Main()

var scheduler = new Scheduler();

// Let the scheduler tick for a moment...
Thread.Sleep(3300);

scheduler.Schedule("0/1 * * * * * *", async schedule =>

Console.WriteLine($"DEBUG: schedule - schedule:ss.fff [Thread.CurrentThread.ManagedThreadId]");
await Task.Delay(1100);
, maxDegreeOfParallelism: Scheduler.UnlimitedJobParallelism);

scheduler.Schedule("0/2 * * * * * *", async schedule =>

Console.WriteLine($"ACTION-1: schedule - schedule:ss.fff");
await Task.Delay(1500);

, maxDegreeOfParallelism: Scheduler.UnlimitedJobParallelism);

scheduler.Schedule(new Job

Name = "Test-job",
CronExpression = "0/3 * * * * * *",
ExecuteAsync = async schedule =>

Console.WriteLine($"ACTION-2: schedule - schedule:ss.fff");
await Task.Delay(4000);
,
MaxDegreeOfParallelism = 1
);







share|improve this question





















  • I'd like to add that the Job validation will be done later with my Simple object validator.
    – t3chb0t
    Apr 15 at 13:34






  • 1




    I have tried this, and it seems to work very well. One comment: because you're awaiting job.ExecuteAsync(...), you don't have to use Task.ContinueWith(...). In an attempt to dive into the Reactive namespace and in an attempt to "solve" the millisecond problem, I have elaborated on you work. Do you mind, if I post a question heavily cannibalising on you ideas?
    – Henrik Hansen
    Apr 16 at 7:34











  • @HenrikHansen not at all. Please do. I'd be happy to see your code and maybe learn something new from it and incorporate it into my solution ;-)
    – t3chb0t
    Apr 16 at 7:37

















up vote
3
down vote

favorite












It would probably be too easy if the old Scheduler worked as intended being that simple. But an eye-openig review showed that from time to time it'll miss one second due to 14ms timer incaccuracy. This of course would have bad implications on the schedules (they wouldn't fire if their second was skipped).




I spent sometime experimenting with various solutions to fix the missing second bug and I think I found a solution that works nicely. I didn't want it to be too complex, I'm not (currently) writing any systems essential for life. It just should make sure nothing is missing. Without it the observable will accumulate all published values and the first subscriber will recieve them all. We don't want this to happen because we are interested at what's now and now in the history.




So how does the Scheduler work? I turned it into a hot observable with Publish + Connect. I want it to be ticking regardles if there is anyone listening.



The new Scheduler also can check how many jobs are already running and decline starting a new one.



How did I fix the missing second issue? The Scheduler is publishing timestamps the same way it did before but now it additionally checks if the difference between the last and the current timestamp is greater then 1s. If it is, it publishes the missing second before publishing the current one. This is achieved by switching from Select to SelectMany. Two intervals published virtually at the same moment don't really matter. More important is to guarantee that nothing is left out and that jobs at any second can be triggered. I think it's extremely unlikely that this would cause any issues ever.



public class Scheduler : IDisposable

public const int UnlimitedJobParallelism = -1;

private const int ZeroJobCounter = 0;

private IConnectableObservable<DateTime> _scheduler;

private IDisposable _disconnect;

private readonly ConcurrentDictionary<Job, int> _jobCounters;

public Scheduler(TimeSpan period, IDateTime dateTime)

var lastTimestamp = dateTime.Now();
_scheduler =
Observable
.Interval(period)
.SelectMany(_ =>

// If we missed one second due to time inaccuracy,
// this makes sure to publish the missing second too
// so that all jobs at that second can also be triggered.
var now = dateTime.Now();
var timestampDifference = now.Second - lastTimestamp.Second;
var timestamps =
Enumerable
// When starting the scheduler it might occur
// that the difference is a negative number (-59).
// If this is the case, make it 1.
.Range(1, timestampDifference < 0 ? 1 : timestampDifference)
.Select(second => lastTimestamp.AddSeconds(second));
lastTimestamp = now;
return timestamps.Dump();
)
// Share the actual data values and not just the observable instance.
.Publish();

// Turn this into a hot observable and start ticking
// regardless if there is anything listening.
_disconnect = _scheduler.Connect();
_jobCounters = new ConcurrentDictionary<Job, int>();


public Scheduler()
: this(TimeSpan.FromSeconds(1), new LocalDateTime())


public IDisposable Schedule(Job job)

var cronExpression = CronExpression.Parse(job.CronExpression);
var unschedule =
_scheduler
.Where(cronExpression.Contains)
.Subscribe(async timestamp =>

if (CanExecute(job))

_jobCounters.AddOrUpdate(job, 1, (j, c) => c + 1);
await job
.ExecuteAsync(timestamp)
.ContinueWith(_ =>
_jobCounters
.AddOrUpdate(job, 1, (j, c) => c - 1)
);

else

$"'job.Name' is alredy running!".Dump();

);

return Disposable.Create(() =>

_jobCounters.TryRemove(job, out var _);
unschedule.Dispose();
);


private bool CanExecute(Job job)

// Reversed the second condition to make the left side consistent.
return
job.MaxDegreeOfParallelism == UnlimitedJobParallelism

public void Dispose()

// Stop ticking.
_disconnect.Dispose();





There are also two new helpers. An extensions for the scheduler and the Job class.



public static class SchedulerExtensions

public static IDisposable Schedule(this Scheduler scheduler, string cronExpression, Func<DateTime, Task> execute, int maxDegreeOfParallelism = 1)

return scheduler.Schedule(new Job

CronExpression = cronExpression,
ExecuteAsync = execute,
MaxDegreeOfParallelism = maxDegreeOfParallelism
);



public class Job

public string Name get; set;

public string CronExpression get; set;

public Func<DateTime, Task> ExecuteAsync get; set;

public int MaxDegreeOfParallelism get; set; = 1;




Example



The usage didn't change much, just the signatures.



void Main()

var scheduler = new Scheduler();

// Let the scheduler tick for a moment...
Thread.Sleep(3300);

scheduler.Schedule("0/1 * * * * * *", async schedule =>

Console.WriteLine($"DEBUG: schedule - schedule:ss.fff [Thread.CurrentThread.ManagedThreadId]");
await Task.Delay(1100);
, maxDegreeOfParallelism: Scheduler.UnlimitedJobParallelism);

scheduler.Schedule("0/2 * * * * * *", async schedule =>

Console.WriteLine($"ACTION-1: schedule - schedule:ss.fff");
await Task.Delay(1500);

, maxDegreeOfParallelism: Scheduler.UnlimitedJobParallelism);

scheduler.Schedule(new Job

Name = "Test-job",
CronExpression = "0/3 * * * * * *",
ExecuteAsync = async schedule =>

Console.WriteLine($"ACTION-2: schedule - schedule:ss.fff");
await Task.Delay(4000);
,
MaxDegreeOfParallelism = 1
);







share|improve this question





















  • I'd like to add that the Job validation will be done later with my Simple object validator.
    – t3chb0t
    Apr 15 at 13:34






  • 1




    I have tried this, and it seems to work very well. One comment: because you're awaiting job.ExecuteAsync(...), you don't have to use Task.ContinueWith(...). In an attempt to dive into the Reactive namespace and in an attempt to "solve" the millisecond problem, I have elaborated on you work. Do you mind, if I post a question heavily cannibalising on you ideas?
    – Henrik Hansen
    Apr 16 at 7:34











  • @HenrikHansen not at all. Please do. I'd be happy to see your code and maybe learn something new from it and incorporate it into my solution ;-)
    – t3chb0t
    Apr 16 at 7:37













up vote
3
down vote

favorite









up vote
3
down vote

favorite











It would probably be too easy if the old Scheduler worked as intended being that simple. But an eye-openig review showed that from time to time it'll miss one second due to 14ms timer incaccuracy. This of course would have bad implications on the schedules (they wouldn't fire if their second was skipped).




I spent sometime experimenting with various solutions to fix the missing second bug and I think I found a solution that works nicely. I didn't want it to be too complex, I'm not (currently) writing any systems essential for life. It just should make sure nothing is missing. Without it the observable will accumulate all published values and the first subscriber will recieve them all. We don't want this to happen because we are interested at what's now and now in the history.




So how does the Scheduler work? I turned it into a hot observable with Publish + Connect. I want it to be ticking regardles if there is anyone listening.



The new Scheduler also can check how many jobs are already running and decline starting a new one.



How did I fix the missing second issue? The Scheduler is publishing timestamps the same way it did before but now it additionally checks if the difference between the last and the current timestamp is greater then 1s. If it is, it publishes the missing second before publishing the current one. This is achieved by switching from Select to SelectMany. Two intervals published virtually at the same moment don't really matter. More important is to guarantee that nothing is left out and that jobs at any second can be triggered. I think it's extremely unlikely that this would cause any issues ever.



public class Scheduler : IDisposable

public const int UnlimitedJobParallelism = -1;

private const int ZeroJobCounter = 0;

private IConnectableObservable<DateTime> _scheduler;

private IDisposable _disconnect;

private readonly ConcurrentDictionary<Job, int> _jobCounters;

public Scheduler(TimeSpan period, IDateTime dateTime)

var lastTimestamp = dateTime.Now();
_scheduler =
Observable
.Interval(period)
.SelectMany(_ =>

// If we missed one second due to time inaccuracy,
// this makes sure to publish the missing second too
// so that all jobs at that second can also be triggered.
var now = dateTime.Now();
var timestampDifference = now.Second - lastTimestamp.Second;
var timestamps =
Enumerable
// When starting the scheduler it might occur
// that the difference is a negative number (-59).
// If this is the case, make it 1.
.Range(1, timestampDifference < 0 ? 1 : timestampDifference)
.Select(second => lastTimestamp.AddSeconds(second));
lastTimestamp = now;
return timestamps.Dump();
)
// Share the actual data values and not just the observable instance.
.Publish();

// Turn this into a hot observable and start ticking
// regardless if there is anything listening.
_disconnect = _scheduler.Connect();
_jobCounters = new ConcurrentDictionary<Job, int>();


public Scheduler()
: this(TimeSpan.FromSeconds(1), new LocalDateTime())


public IDisposable Schedule(Job job)

var cronExpression = CronExpression.Parse(job.CronExpression);
var unschedule =
_scheduler
.Where(cronExpression.Contains)
.Subscribe(async timestamp =>

if (CanExecute(job))

_jobCounters.AddOrUpdate(job, 1, (j, c) => c + 1);
await job
.ExecuteAsync(timestamp)
.ContinueWith(_ =>
_jobCounters
.AddOrUpdate(job, 1, (j, c) => c - 1)
);

else

$"'job.Name' is alredy running!".Dump();

);

return Disposable.Create(() =>

_jobCounters.TryRemove(job, out var _);
unschedule.Dispose();
);


private bool CanExecute(Job job)

// Reversed the second condition to make the left side consistent.
return
job.MaxDegreeOfParallelism == UnlimitedJobParallelism

public void Dispose()

// Stop ticking.
_disconnect.Dispose();





There are also two new helpers. An extensions for the scheduler and the Job class.



public static class SchedulerExtensions

public static IDisposable Schedule(this Scheduler scheduler, string cronExpression, Func<DateTime, Task> execute, int maxDegreeOfParallelism = 1)

return scheduler.Schedule(new Job

CronExpression = cronExpression,
ExecuteAsync = execute,
MaxDegreeOfParallelism = maxDegreeOfParallelism
);



public class Job

public string Name get; set;

public string CronExpression get; set;

public Func<DateTime, Task> ExecuteAsync get; set;

public int MaxDegreeOfParallelism get; set; = 1;




Example



The usage didn't change much, just the signatures.



void Main()

var scheduler = new Scheduler();

// Let the scheduler tick for a moment...
Thread.Sleep(3300);

scheduler.Schedule("0/1 * * * * * *", async schedule =>

Console.WriteLine($"DEBUG: schedule - schedule:ss.fff [Thread.CurrentThread.ManagedThreadId]");
await Task.Delay(1100);
, maxDegreeOfParallelism: Scheduler.UnlimitedJobParallelism);

scheduler.Schedule("0/2 * * * * * *", async schedule =>

Console.WriteLine($"ACTION-1: schedule - schedule:ss.fff");
await Task.Delay(1500);

, maxDegreeOfParallelism: Scheduler.UnlimitedJobParallelism);

scheduler.Schedule(new Job

Name = "Test-job",
CronExpression = "0/3 * * * * * *",
ExecuteAsync = async schedule =>

Console.WriteLine($"ACTION-2: schedule - schedule:ss.fff");
await Task.Delay(4000);
,
MaxDegreeOfParallelism = 1
);







share|improve this question













It would probably be too easy if the old Scheduler worked as intended being that simple. But an eye-openig review showed that from time to time it'll miss one second due to 14ms timer incaccuracy. This of course would have bad implications on the schedules (they wouldn't fire if their second was skipped).




I spent sometime experimenting with various solutions to fix the missing second bug and I think I found a solution that works nicely. I didn't want it to be too complex, I'm not (currently) writing any systems essential for life. It just should make sure nothing is missing. Without it the observable will accumulate all published values and the first subscriber will recieve them all. We don't want this to happen because we are interested at what's now and now in the history.




So how does the Scheduler work? I turned it into a hot observable with Publish + Connect. I want it to be ticking regardles if there is anyone listening.



The new Scheduler also can check how many jobs are already running and decline starting a new one.



How did I fix the missing second issue? The Scheduler is publishing timestamps the same way it did before but now it additionally checks if the difference between the last and the current timestamp is greater then 1s. If it is, it publishes the missing second before publishing the current one. This is achieved by switching from Select to SelectMany. Two intervals published virtually at the same moment don't really matter. More important is to guarantee that nothing is left out and that jobs at any second can be triggered. I think it's extremely unlikely that this would cause any issues ever.



public class Scheduler : IDisposable

public const int UnlimitedJobParallelism = -1;

private const int ZeroJobCounter = 0;

private IConnectableObservable<DateTime> _scheduler;

private IDisposable _disconnect;

private readonly ConcurrentDictionary<Job, int> _jobCounters;

public Scheduler(TimeSpan period, IDateTime dateTime)

var lastTimestamp = dateTime.Now();
_scheduler =
Observable
.Interval(period)
.SelectMany(_ =>

// If we missed one second due to time inaccuracy,
// this makes sure to publish the missing second too
// so that all jobs at that second can also be triggered.
var now = dateTime.Now();
var timestampDifference = now.Second - lastTimestamp.Second;
var timestamps =
Enumerable
// When starting the scheduler it might occur
// that the difference is a negative number (-59).
// If this is the case, make it 1.
.Range(1, timestampDifference < 0 ? 1 : timestampDifference)
.Select(second => lastTimestamp.AddSeconds(second));
lastTimestamp = now;
return timestamps.Dump();
)
// Share the actual data values and not just the observable instance.
.Publish();

// Turn this into a hot observable and start ticking
// regardless if there is anything listening.
_disconnect = _scheduler.Connect();
_jobCounters = new ConcurrentDictionary<Job, int>();


public Scheduler()
: this(TimeSpan.FromSeconds(1), new LocalDateTime())


public IDisposable Schedule(Job job)

var cronExpression = CronExpression.Parse(job.CronExpression);
var unschedule =
_scheduler
.Where(cronExpression.Contains)
.Subscribe(async timestamp =>

if (CanExecute(job))

_jobCounters.AddOrUpdate(job, 1, (j, c) => c + 1);
await job
.ExecuteAsync(timestamp)
.ContinueWith(_ =>
_jobCounters
.AddOrUpdate(job, 1, (j, c) => c - 1)
);

else

$"'job.Name' is alredy running!".Dump();

);

return Disposable.Create(() =>

_jobCounters.TryRemove(job, out var _);
unschedule.Dispose();
);


private bool CanExecute(Job job)

// Reversed the second condition to make the left side consistent.
return
job.MaxDegreeOfParallelism == UnlimitedJobParallelism

public void Dispose()

// Stop ticking.
_disconnect.Dispose();





There are also two new helpers. An extensions for the scheduler and the Job class.



public static class SchedulerExtensions

public static IDisposable Schedule(this Scheduler scheduler, string cronExpression, Func<DateTime, Task> execute, int maxDegreeOfParallelism = 1)

return scheduler.Schedule(new Job

CronExpression = cronExpression,
ExecuteAsync = execute,
MaxDegreeOfParallelism = maxDegreeOfParallelism
);



public class Job

public string Name get; set;

public string CronExpression get; set;

public Func<DateTime, Task> ExecuteAsync get; set;

public int MaxDegreeOfParallelism get; set; = 1;




Example



The usage didn't change much, just the signatures.



void Main()

var scheduler = new Scheduler();

// Let the scheduler tick for a moment...
Thread.Sleep(3300);

scheduler.Schedule("0/1 * * * * * *", async schedule =>

Console.WriteLine($"DEBUG: schedule - schedule:ss.fff [Thread.CurrentThread.ManagedThreadId]");
await Task.Delay(1100);
, maxDegreeOfParallelism: Scheduler.UnlimitedJobParallelism);

scheduler.Schedule("0/2 * * * * * *", async schedule =>

Console.WriteLine($"ACTION-1: schedule - schedule:ss.fff");
await Task.Delay(1500);

, maxDegreeOfParallelism: Scheduler.UnlimitedJobParallelism);

scheduler.Schedule(new Job

Name = "Test-job",
CronExpression = "0/3 * * * * * *",
ExecuteAsync = async schedule =>

Console.WriteLine($"ACTION-2: schedule - schedule:ss.fff");
await Task.Delay(4000);
,
MaxDegreeOfParallelism = 1
);









share|improve this question












share|improve this question




share|improve this question








edited Apr 15 at 9:37
























asked Apr 15 at 9:30









t3chb0t

32k54195




32k54195











  • I'd like to add that the Job validation will be done later with my Simple object validator.
    – t3chb0t
    Apr 15 at 13:34






  • 1




    I have tried this, and it seems to work very well. One comment: because you're awaiting job.ExecuteAsync(...), you don't have to use Task.ContinueWith(...). In an attempt to dive into the Reactive namespace and in an attempt to "solve" the millisecond problem, I have elaborated on you work. Do you mind, if I post a question heavily cannibalising on you ideas?
    – Henrik Hansen
    Apr 16 at 7:34











  • @HenrikHansen not at all. Please do. I'd be happy to see your code and maybe learn something new from it and incorporate it into my solution ;-)
    – t3chb0t
    Apr 16 at 7:37

















  • I'd like to add that the Job validation will be done later with my Simple object validator.
    – t3chb0t
    Apr 15 at 13:34






  • 1




    I have tried this, and it seems to work very well. One comment: because you're awaiting job.ExecuteAsync(...), you don't have to use Task.ContinueWith(...). In an attempt to dive into the Reactive namespace and in an attempt to "solve" the millisecond problem, I have elaborated on you work. Do you mind, if I post a question heavily cannibalising on you ideas?
    – Henrik Hansen
    Apr 16 at 7:34











  • @HenrikHansen not at all. Please do. I'd be happy to see your code and maybe learn something new from it and incorporate it into my solution ;-)
    – t3chb0t
    Apr 16 at 7:37
















I'd like to add that the Job validation will be done later with my Simple object validator.
– t3chb0t
Apr 15 at 13:34




I'd like to add that the Job validation will be done later with my Simple object validator.
– t3chb0t
Apr 15 at 13:34




1




1




I have tried this, and it seems to work very well. One comment: because you're awaiting job.ExecuteAsync(...), you don't have to use Task.ContinueWith(...). In an attempt to dive into the Reactive namespace and in an attempt to "solve" the millisecond problem, I have elaborated on you work. Do you mind, if I post a question heavily cannibalising on you ideas?
– Henrik Hansen
Apr 16 at 7:34





I have tried this, and it seems to work very well. One comment: because you're awaiting job.ExecuteAsync(...), you don't have to use Task.ContinueWith(...). In an attempt to dive into the Reactive namespace and in an attempt to "solve" the millisecond problem, I have elaborated on you work. Do you mind, if I post a question heavily cannibalising on you ideas?
– Henrik Hansen
Apr 16 at 7:34













@HenrikHansen not at all. Please do. I'd be happy to see your code and maybe learn something new from it and incorporate it into my solution ;-)
– t3chb0t
Apr 16 at 7:37





@HenrikHansen not at all. Please do. I'd be happy to see your code and maybe learn something new from it and incorporate it into my solution ;-)
– t3chb0t
Apr 16 at 7:37
















active

oldest

votes











Your Answer




StackExchange.ifUsing("editor", function ()
return StackExchange.using("mathjaxEditing", function ()
StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix)
StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
);
);
, "mathjax-editing");

StackExchange.ifUsing("editor", function ()
StackExchange.using("externalEditor", function ()
StackExchange.using("snippets", function ()
StackExchange.snippets.init();
);
);
, "code-snippets");

StackExchange.ready(function()
var channelOptions =
tags: "".split(" "),
id: "196"
;
initTagRenderer("".split(" "), "".split(" "), channelOptions);

StackExchange.using("externalEditor", function()
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled)
StackExchange.using("snippets", function()
createEditor();
);

else
createEditor();

);

function createEditor()
StackExchange.prepareEditor(
heartbeatType: 'answer',
convertImagesToLinks: false,
noModals: false,
showLowRepImageUploadWarning: true,
reputationToPostImages: null,
bindNavPrevention: true,
postfix: "",
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
);



);








 

draft saved


draft discarded


















StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f192103%2fscheduler-built-with-observables-v2-follow-up%23new-answer', 'question_page');

);

Post as a guest



































active

oldest

votes













active

oldest

votes









active

oldest

votes






active

oldest

votes










 

draft saved


draft discarded


























 


draft saved


draft discarded














StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f192103%2fscheduler-built-with-observables-v2-follow-up%23new-answer', 'question_page');

);

Post as a guest













































































Popular posts from this blog

Chat program with C++ and SFML

Function to Return a JSON Like Objects Using VBA Collections and Arrays

Will my employers contract hold up in court?