OSDN Git Service

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