1 // OpenTween - Client of Twitter
2 // Copyright (c) 2019 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
3 // All rights reserved.
5 // This file is part of OpenTween.
7 // This program is free software; you can redistribute it and/or modify it
8 // under the terms of the GNU General Public License as published by the Free
9 // Software Foundation; either version 3 of the License, or (at your option)
12 // This program is distributed in the hope that it will be useful, but
13 // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14 // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
17 // You should have received a copy of the GNU General Public License along
18 // with this program. If not, see <http://www.gnu.org/licenses/>, or write to
19 // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
20 // Boston, MA 02110-1301, USA.
25 using System.Collections.Generic;
27 using System.Threading;
28 using System.Threading.Tasks;
32 public class TimelineScheduler : IDisposable
34 private static readonly TimelineSchedulerTaskType[] AllTaskTypes =
36 TimelineSchedulerTaskType.Home,
37 TimelineSchedulerTaskType.Mention,
38 TimelineSchedulerTaskType.Dm,
39 TimelineSchedulerTaskType.PublicSearch,
40 TimelineSchedulerTaskType.User,
41 TimelineSchedulerTaskType.List,
42 TimelineSchedulerTaskType.Config,
45 private readonly ITimer timer;
47 private bool enabled = false;
48 private bool systemResumeMode = false;
49 private bool preventTimerUpdate = false;
51 public bool IsDisposed { get; private set; } = false;
58 if (this.enabled == value)
66 public DateTimeUtc SystemResumedAt { get; private set; } = DateTimeUtc.MinValue;
68 public TimeSpan UpdateAfterSystemResume { get; set; } = Timeout.InfiniteTimeSpan;
70 public bool EnableUpdateSystemResume
71 => this.UpdateAfterSystemResume != Timeout.InfiniteTimeSpan;
73 public Dictionary<TimelineSchedulerTaskType, DateTimeUtc> LastUpdatedAt { get; }
74 = new Dictionary<TimelineSchedulerTaskType, DateTimeUtc>();
76 public Dictionary<TimelineSchedulerTaskType, TimeSpan> UpdateInterval { get; }
77 = new Dictionary<TimelineSchedulerTaskType, TimeSpan>();
79 public Dictionary<TimelineSchedulerTaskType, Func<Task>> UpdateFunc { get; }
80 = new Dictionary<TimelineSchedulerTaskType, Func<Task>>();
82 public IEnumerable<TimelineSchedulerTaskType> EnabledTaskTypes
83 => TimelineScheduler.AllTaskTypes.Where(x => this.IsEnabledType(x));
85 public TimelineScheduler()
87 this.timer = this.CreateTimer(this.TimerCallback);
89 foreach (var taskType in TimelineScheduler.AllTaskTypes)
91 this.LastUpdatedAt[taskType] = DateTimeUtc.MinValue;
92 this.UpdateInterval[taskType] = Timeout.InfiniteTimeSpan;
96 protected virtual ITimer CreateTimer(Func<Task> callback)
97 => new AsyncTimer(callback);
99 public bool IsEnabledType(TimelineSchedulerTaskType task)
100 => this.UpdateInterval[task] != Timeout.InfiniteTimeSpan;
102 public void RefreshSchedule()
104 if (this.preventTimerUpdate)
105 return; // TimerCallback 内で更新されるのでここは単に無視してよい
109 var delay = this.NextTimerDelay();
111 // タイマーの待機時間が 1 時間を超える値になった場合は異常値として強制的にリセットする
112 // (タイムライン更新が停止する不具合が報告される件への暫定的な対処)
113 if (delay >= TimeSpan.FromHours(1))
115 MyCommon.ExceptionOut(new Exception("タイムライン更新の待機時間が異常値のためリセットします: " + delay));
116 foreach (var key in this.LastUpdatedAt.Keys)
117 this.LastUpdatedAt[key] = DateTimeUtc.MinValue;
119 delay = TimeSpan.FromSeconds(10);
122 this.timer.Change(delay, Timeout.InfiniteTimeSpan);
126 this.timer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
130 public void SystemResumed()
132 if (!this.EnableUpdateSystemResume)
135 this.SystemResumedAt = DateTimeUtc.Now;
136 this.systemResumeMode = true;
137 this.RefreshSchedule();
142 foreach (var taskType in TimelineScheduler.AllTaskTypes)
143 this.LastUpdatedAt[taskType] = DateTimeUtc.Now;
145 this.systemResumeMode = false;
146 this.RefreshSchedule();
149 private async Task TimerCallback()
153 this.preventTimerUpdate = true;
155 var (taskTypes, updateTasks) = this.systemResumeMode
156 ? this.TimerCallback_AfterSystemResume()
157 : this.TimerCallback_Normal();
159 var updateTask = updateTasks.RunAll(runOnThreadPool: true);
161 // すべてのコールバック関数の Task が完了してから次のタイマーの待機時間を計算する
162 // ただし、30 秒を超過した場合はエラー報告のダイアログを表示した上で完了を待たずにタイマーを再開する
163 // (タイムライン更新が停止する不具合が報告される件への暫定的な対処)
164 var timeout = Task.Delay(TimeSpan.FromSeconds(30));
165 if (await Task.WhenAny(updateTask, timeout) == timeout)
167 var message = "タイムライン更新が規定時間内に完了しませんでした: " +
168 string.Join(", ", taskTypes);
169 throw new Exception(message);
174 this.preventTimerUpdate = false;
175 this.RefreshSchedule();
179 private (TimelineSchedulerTaskType[] TaskTypes, TaskCollection Task) TimerCallback_Normal()
181 var now = DateTimeUtc.Now;
182 var round = TimeSpan.FromSeconds(1); // 1秒未満の差異であればまとめて実行する
183 var tasks = new List<TimelineSchedulerTaskType>(capacity: TimelineScheduler.AllTaskTypes.Length);
185 foreach (var taskType in this.EnabledTaskTypes)
187 var nextScheduledAt = this.LastUpdatedAt[taskType] + this.UpdateInterval[taskType];
188 if (nextScheduledAt - now < round)
192 return (tasks.ToArray(), this.RunUpdateTasks(tasks, now));
195 private (TimelineSchedulerTaskType[] TaskTypes, TaskCollection Task) TimerCallback_AfterSystemResume()
197 // systemResumeMode では一定期間経過後に全てのタイムラインを更新する
198 var now = DateTimeUtc.Now;
200 this.systemResumeMode = false;
201 var taskTypes = TimelineScheduler.AllTaskTypes;
202 return (taskTypes, this.RunUpdateTasks(taskTypes, now));
205 private TaskCollection RunUpdateTasks(IEnumerable<TimelineSchedulerTaskType> taskTypes, DateTimeUtc now)
207 var updateTasks = new TaskCollection(capacity: TimelineScheduler.AllTaskTypes.Length);
209 foreach (var taskType in taskTypes)
211 this.LastUpdatedAt[taskType] = now;
212 if (this.UpdateFunc.TryGetValue(taskType, out var func))
213 updateTasks.Add(func);
219 private TimeSpan NextTimerDelay()
223 if (this.systemResumeMode)
225 // systemResumeMode が有効な間は UpdateAfterSystemResume 以外の設定値を見ない
226 var nextScheduledUpdateAll = this.SystemResumedAt + this.UpdateAfterSystemResume;
227 delay = nextScheduledUpdateAll - DateTimeUtc.Now;
232 var min = DateTimeUtc.MaxValue;
234 foreach (var taskType in this.EnabledTaskTypes)
236 var nextScheduledAt = this.LastUpdatedAt[taskType] + this.UpdateInterval[taskType];
237 if (nextScheduledAt < min)
238 min = nextScheduledAt;
241 if (min == DateTimeUtc.MaxValue)
242 return Timeout.InfiniteTimeSpan;
244 delay = min - DateTimeUtc.Now;
247 return delay > TimeSpan.Zero ? delay : TimeSpan.Zero;
250 protected virtual void Dispose(bool disposing)
256 this.timer.Dispose();
258 this.IsDisposed = true;
261 public void Dispose()
263 this.Dispose(disposing: true);
264 GC.SuppressFinalize(this);
268 public enum TimelineSchedulerTaskType