OSDN Git Service

2006800ef476c8b8ffdacb9137c47a60d3635ce2
[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.Threading;
27 using System.Threading.Tasks;
28
29 namespace OpenTween
30 {
31     public class TimelineScheduler
32     {
33         private readonly AsyncTimer timer;
34
35         private bool enabled = false;
36         private bool systemResumeMode = false;
37         private bool preventTimerUpdate = false;
38
39         public bool Enabled
40         {
41             get => this.enabled;
42             set
43             {
44                 if (this.enabled == value)
45                     return;
46
47                 if (value)
48                 {
49                     var now = DateTimeUtc.Now;
50                     this.LastUpdateHome = now;
51                     this.LastUpdateMention = now;
52                     this.LastUpdateDm = now;
53                     this.LastUpdatePublicSearch = now;
54                     this.LastUpdateUser = now;
55                     this.LastUpdateList = now;
56                     this.LastUpdateConfig = now;
57                 }
58                 this.enabled = value;
59                 this.RefreshSchedule();
60             }
61         }
62
63         public DateTimeUtc LastUpdateHome { get; private set; } = DateTimeUtc.MinValue;
64         public DateTimeUtc LastUpdateMention { get; private set; } = DateTimeUtc.MinValue;
65         public DateTimeUtc LastUpdateDm { get; private set; } = DateTimeUtc.MinValue;
66         public DateTimeUtc LastUpdatePublicSearch { get; private set; } = DateTimeUtc.MinValue;
67         public DateTimeUtc LastUpdateUser { get; private set; } = DateTimeUtc.MinValue;
68         public DateTimeUtc LastUpdateList { get; private set; } = DateTimeUtc.MinValue;
69         public DateTimeUtc LastUpdateConfig { get; private set; } = DateTimeUtc.MinValue;
70         public DateTimeUtc SystemResumedAt { get; private set; } = DateTimeUtc.MinValue;
71
72         public TimeSpan UpdateIntervalHome { get; set; } = Timeout.InfiniteTimeSpan;
73         public TimeSpan UpdateIntervalMention { get; set; } = Timeout.InfiniteTimeSpan;
74         public TimeSpan UpdateIntervalDm { get; set; } = Timeout.InfiniteTimeSpan;
75         public TimeSpan UpdateIntervalPublicSearch { get; set; } = Timeout.InfiniteTimeSpan;
76         public TimeSpan UpdateIntervalUser { get; set; } = Timeout.InfiniteTimeSpan;
77         public TimeSpan UpdateIntervalList { get; set; } = Timeout.InfiniteTimeSpan;
78         public TimeSpan UpdateIntervalConfig { get; set; } = Timeout.InfiniteTimeSpan;
79         public TimeSpan UpdateAfterSystemResume { get; set; } = Timeout.InfiniteTimeSpan;
80
81         public bool EnableUpdateHome
82             => this.UpdateIntervalHome != Timeout.InfiniteTimeSpan;
83
84         public bool EnableUpdateMention
85             => this.UpdateIntervalMention != Timeout.InfiniteTimeSpan;
86
87         public bool EnableUpdateDm
88             => this.UpdateIntervalDm != Timeout.InfiniteTimeSpan;
89
90         public bool EnableUpdatePublicSearch
91             => this.UpdateIntervalPublicSearch != Timeout.InfiniteTimeSpan;
92
93         public bool EnableUpdateUser
94             => this.UpdateIntervalUser != Timeout.InfiniteTimeSpan;
95
96         public bool EnableUpdateList
97             => this.UpdateIntervalList != Timeout.InfiniteTimeSpan;
98
99         public bool EnableUpdateConfig
100             => this.UpdateIntervalConfig != Timeout.InfiniteTimeSpan;
101
102         public bool EnableUpdateSystemResume
103             => this.UpdateAfterSystemResume != Timeout.InfiniteTimeSpan;
104
105         public Func<Task>? UpdateHome;
106         public Func<Task>? UpdateMention;
107         public Func<Task>? UpdateDm;
108         public Func<Task>? UpdatePublicSearch;
109         public Func<Task>? UpdateUser;
110         public Func<Task>? UpdateList;
111         public Func<Task>? UpdateConfig;
112
113         [Flags]
114         private enum UpdateTask
115         {
116             None = 0,
117             Home = 1,
118             Mention = 1 << 2,
119             Dm = 1 << 3,
120             PublicSearch = 1 << 4,
121             User = 1 << 5,
122             List = 1 << 6,
123             Config = 1 << 7,
124             All = Home | Mention | Dm | PublicSearch | User | List | Config,
125         }
126
127         public TimelineScheduler()
128             => this.timer = new AsyncTimer(this.TimerCallback);
129
130         public void RefreshSchedule()
131         {
132             if (this.preventTimerUpdate)
133                 return; // TimerCallback 内で更新されるのでここは単に無視してよい
134
135             if (this.Enabled)
136                 this.timer.Change(this.NextTimerDelay(), Timeout.InfiniteTimeSpan);
137             else
138                 this.timer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
139         }
140
141         public void SystemResumed()
142         {
143             if (!this.EnableUpdateSystemResume)
144                 return;
145
146             this.SystemResumedAt = DateTimeUtc.Now;
147             this.systemResumeMode = true;
148             this.RefreshSchedule();
149         }
150
151         private async Task TimerCallback()
152         {
153             try
154             {
155                 this.preventTimerUpdate = true;
156
157                 if (this.systemResumeMode)
158                     await this.TimerCallback_AfterSystemResume().ConfigureAwait(false);
159                 else
160                     await this.TimerCallback_Normal().ConfigureAwait(false);
161             }
162             finally
163             {
164                 this.preventTimerUpdate = false;
165                 this.RefreshSchedule();
166             }
167         }
168
169         private async Task TimerCallback_Normal()
170         {
171             var now = DateTimeUtc.Now;
172             var round = TimeSpan.FromSeconds(1); // 1秒未満の差異であればまとめて実行する
173             var tasks = UpdateTask.None;
174
175             if (this.EnableUpdateHome)
176             {
177                 var nextScheduledHome = this.LastUpdateHome + this.UpdateIntervalHome;
178                 if (nextScheduledHome - now < round)
179                     tasks |= UpdateTask.Home;
180             }
181
182             if (this.EnableUpdateMention)
183             {
184                 var nextScheduledMention = this.LastUpdateMention + this.UpdateIntervalMention;
185                 if (nextScheduledMention - now < round)
186                     tasks |= UpdateTask.Mention;
187             }
188
189             if (this.EnableUpdateDm)
190             {
191                 var nextScheduledDm = this.LastUpdateDm + this.UpdateIntervalDm;
192                 if (nextScheduledDm - now < round)
193                     tasks |= UpdateTask.Dm;
194             }
195
196             if (this.EnableUpdatePublicSearch)
197             {
198                 var nextScheduledPublicSearch = this.LastUpdatePublicSearch + this.UpdateIntervalPublicSearch;
199                 if (nextScheduledPublicSearch - now < round)
200                     tasks |= UpdateTask.PublicSearch;
201             }
202
203             if (this.EnableUpdateUser)
204             {
205                 var nextScheduledUser = this.LastUpdateUser + this.UpdateIntervalUser;
206                 if (nextScheduledUser - now < round)
207                     tasks |= UpdateTask.User;
208             }
209
210             if (this.EnableUpdateList)
211             {
212                 var nextScheduledList = this.LastUpdateList + this.UpdateIntervalList;
213                 if (nextScheduledList - now < round)
214                     tasks |= UpdateTask.List;
215             }
216
217             if (this.EnableUpdateConfig)
218             {
219                 var nextScheduledConfig = this.LastUpdateConfig + this.UpdateIntervalConfig;
220                 if (nextScheduledConfig - now < round)
221                     tasks |= UpdateTask.Config;
222             }
223
224             await this.RunUpdateTasks(tasks, now).ConfigureAwait(false);
225         }
226
227         private async Task TimerCallback_AfterSystemResume()
228         {
229             // systemResumeMode では一定期間経過後に全てのタイムラインを更新する
230             var now = DateTimeUtc.Now;
231
232             var nextScheduledUpdateAll = this.SystemResumedAt + this.UpdateAfterSystemResume;
233             if (nextScheduledUpdateAll - now < TimeSpan.Zero)
234             {
235                 this.systemResumeMode = false;
236                 await this.RunUpdateTasks(UpdateTask.All, now).ConfigureAwait(false);
237             }
238         }
239
240         private async Task RunUpdateTasks(UpdateTask tasks, DateTimeUtc now)
241         {
242             var updateTasks = new List<Task>(capacity: 7);
243
244             // LastUpdate* を次の時刻に更新してから Update* を実行すること
245             // (LastUpdate* が更新されずに Update* が例外を投げると無限ループに陥る)
246
247             if ((tasks & UpdateTask.Home) == UpdateTask.Home)
248             {
249                 this.LastUpdateHome = now;
250                 if (this.UpdateHome != null)
251                     updateTasks.Add(this.UpdateHome());
252             }
253
254             if ((tasks & UpdateTask.Mention) == UpdateTask.Mention)
255             {
256                 this.LastUpdateMention = now;
257                 if (this.UpdateMention != null)
258                     updateTasks.Add(this.UpdateMention());
259             }
260
261             if ((tasks & UpdateTask.Dm) == UpdateTask.Dm)
262             {
263                 this.LastUpdateDm = now;
264                 if (this.UpdateDm != null)
265                     updateTasks.Add(this.UpdateDm());
266             }
267
268             if ((tasks & UpdateTask.PublicSearch) == UpdateTask.PublicSearch)
269             {
270                 this.LastUpdatePublicSearch = now;
271                 if (this.UpdatePublicSearch != null)
272                     updateTasks.Add(this.UpdatePublicSearch());
273             }
274
275             if ((tasks & UpdateTask.User) == UpdateTask.User)
276             {
277                 this.LastUpdateUser = now;
278                 if (this.UpdateUser != null)
279                     updateTasks.Add(this.UpdateUser());
280             }
281
282             if ((tasks & UpdateTask.List) == UpdateTask.List)
283             {
284                 this.LastUpdateList = now;
285                 if (this.UpdateList != null)
286                     updateTasks.Add(this.UpdateList());
287             }
288
289             if ((tasks & UpdateTask.Config) == UpdateTask.Config)
290             {
291                 this.LastUpdateConfig = now;
292                 if (this.UpdateConfig != null)
293                     updateTasks.Add(this.UpdateConfig());
294             }
295
296             await Task.WhenAll(updateTasks).ConfigureAwait(false);
297         }
298
299         private TimeSpan NextTimerDelay()
300         {
301             TimeSpan delay;
302
303             if (this.systemResumeMode)
304             {
305                 // systemResumeMode が有効な間は UpdateAfterSystemResume 以外の設定値を見ない
306                 var nextScheduledUpdateAll = this.SystemResumedAt + this.UpdateAfterSystemResume;
307                 delay = nextScheduledUpdateAll - DateTimeUtc.Now;
308
309                 return delay > TimeSpan.Zero ? delay : TimeSpan.Zero;
310             }
311
312             // 次に更新が予定される時刻を判定する
313             var min = DateTimeUtc.MaxValue;
314
315             if (this.EnableUpdateHome)
316             {
317                 var nextScheduledHome = this.LastUpdateHome + this.UpdateIntervalHome;
318                 if (nextScheduledHome < min)
319                     min = nextScheduledHome;
320             }
321
322             if (this.EnableUpdateMention)
323             {
324                 var nextScheduledMention = this.LastUpdateMention + this.UpdateIntervalMention;
325                 if (nextScheduledMention < min)
326                     min = nextScheduledMention;
327             }
328
329             if (this.EnableUpdateDm)
330             {
331                 var nextScheduledDm = this.LastUpdateDm + this.UpdateIntervalDm;
332                 if (nextScheduledDm < min)
333                     min = nextScheduledDm;
334             }
335
336             if (this.EnableUpdatePublicSearch)
337             {
338                 var nextScheduledPublicSearch = this.LastUpdatePublicSearch + this.UpdateIntervalPublicSearch;
339                 if (nextScheduledPublicSearch < min)
340                     min = nextScheduledPublicSearch;
341             }
342
343             if (this.EnableUpdateUser)
344             {
345                 var nextScheduledUser = this.LastUpdateUser + this.UpdateIntervalUser;
346                 if (nextScheduledUser < min)
347                     min = nextScheduledUser;
348             }
349
350             if (this.EnableUpdateList)
351             {
352                 var nextScheduledList = this.LastUpdateList + this.UpdateIntervalList;
353                 if (nextScheduledList < min)
354                     min = nextScheduledList;
355             }
356
357             if (this.EnableUpdateConfig)
358             {
359                 var nextScheduledConfig = this.LastUpdateConfig + this.UpdateIntervalConfig;
360                 if (nextScheduledConfig < min)
361                     min = nextScheduledConfig;
362             }
363
364             delay = min - DateTimeUtc.Now;
365
366             return delay > TimeSpan.Zero ? delay : TimeSpan.Zero;
367         }
368     }
369 }