// OpenTween - Client of Twitter
// Copyright (c) 2019 kim_upsilon (@kim_upsilon)
// All rights reserved.
//
// This file is part of OpenTween.
//
// This program is free software; you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation; either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
// for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program. If not, see , or write to
// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
// Boston, MA 02110-1301, USA.
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace OpenTween
{
public class TimelineScheduler : IDisposable
{
private static readonly TimelineSchedulerTaskType[] AllTaskTypes =
{
TimelineSchedulerTaskType.Home,
TimelineSchedulerTaskType.Mention,
TimelineSchedulerTaskType.Dm,
TimelineSchedulerTaskType.PublicSearch,
TimelineSchedulerTaskType.User,
TimelineSchedulerTaskType.List,
TimelineSchedulerTaskType.Config,
};
private readonly ITimer timer;
private bool enabled = false;
private bool systemResumeMode = false;
private bool preventTimerUpdate = false;
public bool IsDisposed { get; private set; } = false;
public bool Enabled
{
get => this.enabled;
set
{
if (this.enabled == value)
return;
this.enabled = value;
this.Reset();
}
}
public DateTimeUtc SystemResumedAt { get; private set; } = DateTimeUtc.MinValue;
public TimeSpan UpdateAfterSystemResume { get; set; } = Timeout.InfiniteTimeSpan;
public bool EnableUpdateSystemResume
=> this.UpdateAfterSystemResume != Timeout.InfiniteTimeSpan;
public Dictionary LastUpdatedAt { get; }
= new Dictionary();
public Dictionary UpdateInterval { get; }
= new Dictionary();
public Dictionary> UpdateFunc { get; }
= new Dictionary>();
public IEnumerable EnabledTaskTypes
=> TimelineScheduler.AllTaskTypes.Where(x => this.IsEnabledType(x));
public TimelineScheduler()
{
this.timer = this.CreateTimer(this.TimerCallback);
foreach (var taskType in TimelineScheduler.AllTaskTypes)
{
this.LastUpdatedAt[taskType] = DateTimeUtc.MinValue;
this.UpdateInterval[taskType] = Timeout.InfiniteTimeSpan;
}
}
protected virtual ITimer CreateTimer(Func callback)
=> new AsyncTimer(callback);
public bool IsEnabledType(TimelineSchedulerTaskType task)
=> this.UpdateInterval[task] != Timeout.InfiniteTimeSpan;
public void RefreshSchedule()
{
if (this.preventTimerUpdate)
return; // TimerCallback 内で更新されるのでここは単に無視してよい
if (this.Enabled)
this.timer.Change(this.NextTimerDelay(), Timeout.InfiniteTimeSpan);
else
this.timer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
}
public void SystemResumed()
{
if (!this.EnableUpdateSystemResume)
return;
this.SystemResumedAt = DateTimeUtc.Now;
this.systemResumeMode = true;
this.RefreshSchedule();
}
public void Reset()
{
foreach (var taskType in TimelineScheduler.AllTaskTypes)
this.LastUpdatedAt[taskType] = DateTimeUtc.MinValue;
this.systemResumeMode = false;
this.RefreshSchedule();
}
private async Task TimerCallback()
{
try
{
this.preventTimerUpdate = true;
if (this.systemResumeMode)
await this.TimerCallback_AfterSystemResume().ConfigureAwait(false);
else
await this.TimerCallback_Normal().ConfigureAwait(false);
}
finally
{
this.preventTimerUpdate = false;
this.RefreshSchedule();
}
}
private async Task TimerCallback_Normal()
{
var now = DateTimeUtc.Now;
var round = TimeSpan.FromSeconds(1); // 1秒未満の差異であればまとめて実行する
var tasks = new List(capacity: TimelineScheduler.AllTaskTypes.Length);
foreach (var taskType in this.EnabledTaskTypes)
{
var nextScheduledAt = this.LastUpdatedAt[taskType] + this.UpdateInterval[taskType];
if (nextScheduledAt - now < round)
tasks.Add(taskType);
}
await this.RunUpdateTasks(tasks, now).ConfigureAwait(false);
}
private async Task TimerCallback_AfterSystemResume()
{
// systemResumeMode では一定期間経過後に全てのタイムラインを更新する
var now = DateTimeUtc.Now;
this.systemResumeMode = false;
await this.RunUpdateTasks(TimelineScheduler.AllTaskTypes, now).ConfigureAwait(false);
}
private async Task RunUpdateTasks(IEnumerable taskTypes, DateTimeUtc now)
{
var updateTasks = new List>(capacity: TimelineScheduler.AllTaskTypes.Length);
foreach (var taskType in taskTypes)
{
this.LastUpdatedAt[taskType] = now;
if (this.UpdateFunc.TryGetValue(taskType, out var func))
updateTasks.Add(func);
}
await Task.WhenAll(updateTasks.Select(x => Task.Run(x)))
.ConfigureAwait(false);
}
private TimeSpan NextTimerDelay()
{
TimeSpan delay;
if (this.systemResumeMode)
{
// systemResumeMode が有効な間は UpdateAfterSystemResume 以外の設定値を見ない
var nextScheduledUpdateAll = this.SystemResumedAt + this.UpdateAfterSystemResume;
delay = nextScheduledUpdateAll - DateTimeUtc.Now;
}
else
{
// 次に更新が予定される時刻を判定する
var min = DateTimeUtc.MaxValue;
foreach (var taskType in this.EnabledTaskTypes)
{
var nextScheduledAt = this.LastUpdatedAt[taskType] + this.UpdateInterval[taskType];
if (nextScheduledAt < min)
min = nextScheduledAt;
}
if (min == DateTimeUtc.MaxValue)
return Timeout.InfiniteTimeSpan;
delay = min - DateTimeUtc.Now;
}
return delay > TimeSpan.Zero ? delay : TimeSpan.Zero;
}
protected virtual void Dispose(bool disposing)
{
if (this.IsDisposed)
return;
if (disposing)
this.timer.Dispose();
this.IsDisposed = true;
}
public void Dispose()
{
this.Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
public enum TimelineSchedulerTaskType
{
Home,
Mention,
Dm,
PublicSearch,
User,
List,
Config,
}
}