OSDN Git Service

graphqlエンドポイント使用時にツイート検索の言語指定が効かない不具合を修正
[opentween/open-tween.git] / OpenTween / TimelineScheduler.cs
1 // OpenTween - Client of Twitter
2 // Copyright (c) 2019 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
3 // All rights reserved.
4 //
5 // This file is part of OpenTween.
6 //
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)
10 // any later version.
11 //
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
15 // for more details.
16 //
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.
21
22 #nullable enable
23
24 using System;
25 using System.Collections.Generic;
26 using System.Linq;
27 using System.Threading;
28 using System.Threading.Tasks;
29
30 namespace OpenTween
31 {
32     public class TimelineScheduler : IDisposable
33     {
34         private static readonly TimelineSchedulerTaskType[] AllTaskTypes =
35         {
36             TimelineSchedulerTaskType.Home,
37             TimelineSchedulerTaskType.Mention,
38             TimelineSchedulerTaskType.Dm,
39             TimelineSchedulerTaskType.PublicSearch,
40             TimelineSchedulerTaskType.User,
41             TimelineSchedulerTaskType.List,
42             TimelineSchedulerTaskType.Config,
43         };
44
45         private readonly ITimer timer;
46
47         private bool enabled = false;
48         private bool systemResumeMode = false;
49         private bool preventTimerUpdate = false;
50
51         public bool IsDisposed { get; private set; } = false;
52
53         public bool Enabled
54         {
55             get => this.enabled;
56             set
57             {
58                 if (this.enabled == value)
59                     return;
60
61                 this.enabled = value;
62                 this.Reset();
63             }
64         }
65
66         public DateTimeUtc SystemResumedAt { get; private set; } = DateTimeUtc.MinValue;
67
68         public TimeSpan UpdateAfterSystemResume { get; set; } = Timeout.InfiniteTimeSpan;
69
70         public bool EnableUpdateSystemResume
71             => this.UpdateAfterSystemResume != Timeout.InfiniteTimeSpan;
72
73         public Dictionary<TimelineSchedulerTaskType, DateTimeUtc> LastUpdatedAt { get; }
74             = new Dictionary<TimelineSchedulerTaskType, DateTimeUtc>();
75
76         public Dictionary<TimelineSchedulerTaskType, TimeSpan> UpdateInterval { get; }
77             = new Dictionary<TimelineSchedulerTaskType, TimeSpan>();
78
79         public Dictionary<TimelineSchedulerTaskType, Func<Task>> UpdateFunc { get; }
80             = new Dictionary<TimelineSchedulerTaskType, Func<Task>>();
81
82         public IEnumerable<TimelineSchedulerTaskType> EnabledTaskTypes
83             => TimelineScheduler.AllTaskTypes.Where(x => this.IsEnabledType(x));
84
85         public TimelineScheduler()
86         {
87             this.timer = this.CreateTimer(this.TimerCallback);
88
89             foreach (var taskType in TimelineScheduler.AllTaskTypes)
90             {
91                 this.LastUpdatedAt[taskType] = DateTimeUtc.MinValue;
92                 this.UpdateInterval[taskType] = Timeout.InfiniteTimeSpan;
93             }
94         }
95
96         protected virtual ITimer CreateTimer(Func<Task> callback)
97             => new AsyncTimer(callback);
98
99         public bool IsEnabledType(TimelineSchedulerTaskType task)
100             => this.UpdateInterval[task] != Timeout.InfiniteTimeSpan;
101
102         public void RefreshSchedule()
103         {
104             if (this.preventTimerUpdate)
105                 return; // TimerCallback 内で更新されるのでここは単に無視してよい
106
107             if (this.Enabled)
108             {
109                 var delay = this.NextTimerDelay();
110
111                 // タイマーの待機時間が 1 時間を超える値になった場合は異常値として強制的にリセットする
112                 // (タイムライン更新が停止する不具合が報告される件への暫定的な対処)
113                 if (delay >= TimeSpan.FromHours(1))
114                 {
115                     MyCommon.ExceptionOut(new Exception("タイムライン更新の待機時間が異常値のためリセットします: " + delay));
116                     foreach (var key in this.LastUpdatedAt.Keys)
117                         this.LastUpdatedAt[key] = DateTimeUtc.MinValue;
118
119                     delay = TimeSpan.FromSeconds(10);
120                 }
121
122                 this.timer.Change(delay, Timeout.InfiniteTimeSpan);
123             }
124             else
125             {
126                 this.timer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
127             }
128         }
129
130         public void SystemResumed()
131         {
132             if (!this.EnableUpdateSystemResume)
133                 return;
134
135             this.SystemResumedAt = DateTimeUtc.Now;
136             this.systemResumeMode = true;
137             this.RefreshSchedule();
138         }
139
140         public void Reset()
141         {
142             foreach (var taskType in TimelineScheduler.AllTaskTypes)
143                 this.LastUpdatedAt[taskType] = DateTimeUtc.Now;
144
145             this.systemResumeMode = false;
146             this.RefreshSchedule();
147         }
148
149         private async Task TimerCallback()
150         {
151             try
152             {
153                 this.preventTimerUpdate = true;
154
155                 var (taskTypes, updateTasks) = this.systemResumeMode
156                     ? this.TimerCallback_AfterSystemResume()
157                     : this.TimerCallback_Normal();
158
159                 var updateTask = updateTasks.RunAll(runOnThreadPool: true);
160
161                 // すべてのコールバック関数の Task が完了してから次のタイマーの待機時間を計算する
162                 // ただし、30 秒を超過した場合はエラー報告のダイアログを表示した上で完了を待たずにタイマーを再開する
163                 // (タイムライン更新が停止する不具合が報告される件への暫定的な対処)
164                 var timeout = Task.Delay(TimeSpan.FromSeconds(30));
165                 if (await Task.WhenAny(updateTask, timeout) == timeout)
166                 {
167                     var message = "タイムライン更新が規定時間内に完了しませんでした: " +
168                         string.Join(", ", taskTypes);
169                     throw new Exception(message);
170                 }
171             }
172             finally
173             {
174                 this.preventTimerUpdate = false;
175                 this.RefreshSchedule();
176             }
177         }
178
179         private (TimelineSchedulerTaskType[] TaskTypes, TaskCollection Task) TimerCallback_Normal()
180         {
181             var now = DateTimeUtc.Now;
182             var round = TimeSpan.FromSeconds(1); // 1秒未満の差異であればまとめて実行する
183             var tasks = new List<TimelineSchedulerTaskType>(capacity: TimelineScheduler.AllTaskTypes.Length);
184
185             foreach (var taskType in this.EnabledTaskTypes)
186             {
187                 var nextScheduledAt = this.LastUpdatedAt[taskType] + this.UpdateInterval[taskType];
188                 if (nextScheduledAt - now < round)
189                     tasks.Add(taskType);
190             }
191
192             return (tasks.ToArray(), this.RunUpdateTasks(tasks, now));
193         }
194
195         private (TimelineSchedulerTaskType[] TaskTypes, TaskCollection Task) TimerCallback_AfterSystemResume()
196         {
197             // systemResumeMode では一定期間経過後に全てのタイムラインを更新する
198             var now = DateTimeUtc.Now;
199
200             this.systemResumeMode = false;
201             var taskTypes = TimelineScheduler.AllTaskTypes;
202             return (taskTypes, this.RunUpdateTasks(taskTypes, now));
203         }
204
205         private TaskCollection RunUpdateTasks(IEnumerable<TimelineSchedulerTaskType> taskTypes, DateTimeUtc now)
206         {
207             var updateTasks = new TaskCollection(capacity: TimelineScheduler.AllTaskTypes.Length);
208
209             foreach (var taskType in taskTypes)
210             {
211                 this.LastUpdatedAt[taskType] = now;
212                 if (this.UpdateFunc.TryGetValue(taskType, out var func))
213                     updateTasks.Add(func);
214             }
215
216             return updateTasks;
217         }
218
219         private TimeSpan NextTimerDelay()
220         {
221             TimeSpan delay;
222
223             if (this.systemResumeMode)
224             {
225                 // systemResumeMode が有効な間は UpdateAfterSystemResume 以外の設定値を見ない
226                 var nextScheduledUpdateAll = this.SystemResumedAt + this.UpdateAfterSystemResume;
227                 delay = nextScheduledUpdateAll - DateTimeUtc.Now;
228             }
229             else
230             {
231                 // 次に更新が予定される時刻を判定する
232                 var min = DateTimeUtc.MaxValue;
233
234                 foreach (var taskType in this.EnabledTaskTypes)
235                 {
236                     var nextScheduledAt = this.LastUpdatedAt[taskType] + this.UpdateInterval[taskType];
237                     if (nextScheduledAt < min)
238                         min = nextScheduledAt;
239                 }
240
241                 if (min == DateTimeUtc.MaxValue)
242                     return Timeout.InfiniteTimeSpan;
243
244                 delay = min - DateTimeUtc.Now;
245             }
246
247             return delay > TimeSpan.Zero ? delay : TimeSpan.Zero;
248         }
249
250         protected virtual void Dispose(bool disposing)
251         {
252             if (this.IsDisposed)
253                 return;
254
255             if (disposing)
256                 this.timer.Dispose();
257
258             this.IsDisposed = true;
259         }
260
261         public void Dispose()
262         {
263             this.Dispose(disposing: true);
264             GC.SuppressFinalize(this);
265         }
266     }
267
268     public enum TimelineSchedulerTaskType
269     {
270         Home,
271         Mention,
272         Dm,
273         PublicSearch,
274         User,
275         List,
276         Config,
277     }
278 }