// 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.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace OpenTween
{
public class TimelineScheduler
{
private readonly Timer timer;
private bool enabled = false;
private bool systemResumeMode = false;
private bool preventTimerUpdate = false;
public bool Enabled
{
get => this.enabled;
set
{
if (this.enabled == value)
return;
if (value)
{
var now = DateTimeUtc.Now;
this.LastUpdateHome = now;
this.LastUpdateMention = now;
this.LastUpdateDm = now;
this.LastUpdatePublicSearch = now;
this.LastUpdateUser = now;
this.LastUpdateList = now;
this.LastUpdateConfig = now;
}
this.enabled = value;
this.RefreshSchedule();
}
}
public DateTimeUtc LastUpdateHome { get; private set; } = DateTimeUtc.MinValue;
public DateTimeUtc LastUpdateMention { get; private set; } = DateTimeUtc.MinValue;
public DateTimeUtc LastUpdateDm { get; private set; } = DateTimeUtc.MinValue;
public DateTimeUtc LastUpdatePublicSearch { get; private set; } = DateTimeUtc.MinValue;
public DateTimeUtc LastUpdateUser { get; private set; } = DateTimeUtc.MinValue;
public DateTimeUtc LastUpdateList { get; private set; } = DateTimeUtc.MinValue;
public DateTimeUtc LastUpdateConfig { get; private set; } = DateTimeUtc.MinValue;
public DateTimeUtc SystemResumedAt { get; private set; } = DateTimeUtc.MinValue;
public TimeSpan UpdateIntervalHome { get; set; } = Timeout.InfiniteTimeSpan;
public TimeSpan UpdateIntervalMention { get; set; } = Timeout.InfiniteTimeSpan;
public TimeSpan UpdateIntervalDm { get; set; } = Timeout.InfiniteTimeSpan;
public TimeSpan UpdateIntervalPublicSearch { get; set; } = Timeout.InfiniteTimeSpan;
public TimeSpan UpdateIntervalUser { get; set; } = Timeout.InfiniteTimeSpan;
public TimeSpan UpdateIntervalList { get; set; } = Timeout.InfiniteTimeSpan;
public TimeSpan UpdateIntervalConfig { get; set; } = Timeout.InfiniteTimeSpan;
public TimeSpan UpdateAfterSystemResume { get; set; } = Timeout.InfiniteTimeSpan;
public Func UpdateHome;
public Func UpdateMention;
public Func UpdateDm;
public Func UpdatePublicSearch;
public Func UpdateUser;
public Func UpdateList;
public Func UpdateConfig;
[Flags]
private enum UpdateTask
{
None = 0,
Home = 1,
Mention = 1 << 2,
Dm = 1 << 3,
PublicSearch = 1 << 4,
User = 1 << 5,
List = 1 << 6,
Config = 1 << 7,
All = Home | Mention | Dm | PublicSearch | User | List | Config,
}
public TimelineScheduler()
=> this.timer = new Timer(_ => this.TimerCallback());
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()
{
this.SystemResumedAt = DateTimeUtc.Now;
this.systemResumeMode = true;
this.RefreshSchedule();
}
private async void 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 = UpdateTask.None;
var nextScheduledHome = this.LastUpdateHome + this.UpdateIntervalHome;
if (nextScheduledHome - now < round)
tasks |= UpdateTask.Home;
var nextScheduledMention = this.LastUpdateMention + this.UpdateIntervalMention;
if (nextScheduledMention - now < round)
tasks |= UpdateTask.Mention;
var nextScheduledDm = this.LastUpdateDm + this.UpdateIntervalDm;
if (nextScheduledDm - now < round)
tasks |= UpdateTask.Dm;
var nextScheduledPublicSearch = this.LastUpdatePublicSearch + this.UpdateIntervalPublicSearch;
if (nextScheduledPublicSearch - now < round)
tasks |= UpdateTask.PublicSearch;
var nextScheduledUser = this.LastUpdateUser + this.UpdateIntervalUser;
if (nextScheduledUser - now < round)
tasks |= UpdateTask.User;
var nextScheduledList = this.LastUpdateList + this.UpdateIntervalList;
if (nextScheduledList - now < round)
tasks |= UpdateTask.List;
var nextScheduledConfig = this.LastUpdateConfig + this.UpdateIntervalConfig;
if (nextScheduledConfig - now < round)
tasks |= UpdateTask.Config;
await this.RunUpdateTasks(tasks, now).ConfigureAwait(false);
}
private async Task TimerCallback_AfterSystemResume()
{
// systemResumeMode では一定期間経過後に全てのタイムラインを更新する
var now = DateTimeUtc.Now;
var nextScheduledUpdateAll = this.SystemResumedAt + this.UpdateAfterSystemResume;
if (nextScheduledUpdateAll - now < TimeSpan.Zero)
{
this.systemResumeMode = false;
await this.RunUpdateTasks(UpdateTask.All, now).ConfigureAwait(false);
}
}
private async Task RunUpdateTasks(UpdateTask tasks, DateTimeUtc now)
{
var updateTasks = new List(capacity: 7);
// LastUpdate* を次の時刻に更新してから Update* を実行すること
// (LastUpdate* が更新されずに Update* が例外を投げると無限ループに陥る)
if ((tasks & UpdateTask.Home) == UpdateTask.Home)
{
this.LastUpdateHome = now;
if (this.UpdateHome != null)
updateTasks.Add(this.UpdateHome());
}
if ((tasks & UpdateTask.Mention) == UpdateTask.Mention)
{
this.LastUpdateMention = now;
if (this.UpdateMention != null)
updateTasks.Add(this.UpdateMention());
}
if ((tasks & UpdateTask.Dm) == UpdateTask.Dm)
{
this.LastUpdateDm = now;
if (this.UpdateDm != null)
updateTasks.Add(this.UpdateDm());
}
if ((tasks & UpdateTask.PublicSearch) == UpdateTask.PublicSearch)
{
this.LastUpdatePublicSearch = now;
if (this.UpdatePublicSearch != null)
updateTasks.Add(this.UpdatePublicSearch());
}
if ((tasks & UpdateTask.User) == UpdateTask.User)
{
this.LastUpdateUser = now;
if (this.UpdateUser != null)
updateTasks.Add(this.UpdateUser());
}
if ((tasks & UpdateTask.List) == UpdateTask.List)
{
this.LastUpdateList = now;
if (this.UpdateList != null)
updateTasks.Add(this.UpdateList());
}
if ((tasks & UpdateTask.Config) == UpdateTask.Config)
{
this.LastUpdateConfig = now;
if (this.UpdateConfig != null)
updateTasks.Add(this.UpdateConfig());
}
await Task.WhenAll(updateTasks).ConfigureAwait(false);
}
private TimeSpan NextTimerDelay()
{
TimeSpan delay;
if (this.systemResumeMode)
{
// systemResumeMode が有効な間は UpdateAfterSystemResume 以外の設定値を見ない
var nextScheduledUpdateAll = this.SystemResumedAt + this.UpdateAfterSystemResume;
delay = nextScheduledUpdateAll - DateTimeUtc.Now;
return delay > TimeSpan.Zero ? delay : TimeSpan.Zero;
}
// 次に更新が予定される時刻を判定する
var min = DateTimeUtc.MaxValue;
var nextScheduledHome = this.LastUpdateHome + this.UpdateIntervalHome;
if (nextScheduledHome < min)
min = nextScheduledHome;
var nextScheduledMention = this.LastUpdateMention + this.UpdateIntervalMention;
if (nextScheduledMention < min)
min = nextScheduledMention;
var nextScheduledDm = this.LastUpdateDm + this.UpdateIntervalDm;
if (nextScheduledDm < min)
min = nextScheduledDm;
var nextScheduledPublicSearch = this.LastUpdatePublicSearch + this.UpdateIntervalPublicSearch;
if (nextScheduledPublicSearch < min)
min = nextScheduledPublicSearch;
var nextScheduledUser = this.LastUpdateUser + this.UpdateIntervalUser;
if (nextScheduledUser < min)
min = nextScheduledUser;
var nextScheduledList = this.LastUpdateList + this.UpdateIntervalList;
if (nextScheduledList < min)
min = nextScheduledList;
var nextScheduledConfig = this.LastUpdateConfig + this.UpdateIntervalConfig;
if (nextScheduledConfig < min)
min = nextScheduledConfig;
delay = min - DateTimeUtc.Now;
return delay > TimeSpan.Zero ? delay : TimeSpan.Zero;
}
}
}