OSDN Git Service

Merge pull request #76 from upsilon/csharp8
[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 Timer 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 Func<Task>? UpdateHome;
82         public Func<Task>? UpdateMention;
83         public Func<Task>? UpdateDm;
84         public Func<Task>? UpdatePublicSearch;
85         public Func<Task>? UpdateUser;
86         public Func<Task>? UpdateList;
87         public Func<Task>? UpdateConfig;
88
89         [Flags]
90         private enum UpdateTask
91         {
92             None = 0,
93             Home = 1,
94             Mention = 1 << 2,
95             Dm = 1 << 3,
96             PublicSearch = 1 << 4,
97             User = 1 << 5,
98             List = 1 << 6,
99             Config = 1 << 7,
100             All = Home | Mention | Dm | PublicSearch | User | List | Config,
101         }
102
103         public TimelineScheduler()
104             => this.timer = new Timer(_ => this.TimerCallback());
105
106         public void RefreshSchedule()
107         {
108             if (this.preventTimerUpdate)
109                 return; // TimerCallback 内で更新されるのでここは単に無視してよい
110
111             if (this.Enabled)
112                 this.timer.Change(this.NextTimerDelay(), Timeout.InfiniteTimeSpan);
113             else
114                 this.timer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
115         }
116
117         public void SystemResumed()
118         {
119             this.SystemResumedAt = DateTimeUtc.Now;
120             this.systemResumeMode = true;
121             this.RefreshSchedule();
122         }
123
124         private async void TimerCallback()
125         {
126             try
127             {
128                 this.preventTimerUpdate = true;
129
130                 if (this.systemResumeMode)
131                     await this.TimerCallback_AfterSystemResume().ConfigureAwait(false);
132                 else
133                     await this.TimerCallback_Normal().ConfigureAwait(false);
134             }
135             finally
136             {
137                 this.preventTimerUpdate = false;
138                 this.RefreshSchedule();
139             }
140         }
141
142         private async Task TimerCallback_Normal()
143         {
144             var now = DateTimeUtc.Now;
145             var round = TimeSpan.FromSeconds(1); // 1秒未満の差異であればまとめて実行する
146             var tasks = UpdateTask.None;
147
148             var nextScheduledHome = this.LastUpdateHome + this.UpdateIntervalHome;
149             if (nextScheduledHome - now < round)
150                 tasks |= UpdateTask.Home;
151
152             var nextScheduledMention = this.LastUpdateMention + this.UpdateIntervalMention;
153             if (nextScheduledMention - now < round)
154                 tasks |= UpdateTask.Mention;
155
156             var nextScheduledDm = this.LastUpdateDm + this.UpdateIntervalDm;
157             if (nextScheduledDm - now < round)
158                 tasks |= UpdateTask.Dm;
159
160             var nextScheduledPublicSearch = this.LastUpdatePublicSearch + this.UpdateIntervalPublicSearch;
161             if (nextScheduledPublicSearch - now < round)
162                 tasks |= UpdateTask.PublicSearch;
163
164             var nextScheduledUser = this.LastUpdateUser + this.UpdateIntervalUser;
165             if (nextScheduledUser - now < round)
166                 tasks |= UpdateTask.User;
167
168             var nextScheduledList = this.LastUpdateList + this.UpdateIntervalList;
169             if (nextScheduledList - now < round)
170                 tasks |= UpdateTask.List;
171
172             var nextScheduledConfig = this.LastUpdateConfig + this.UpdateIntervalConfig;
173             if (nextScheduledConfig - now < round)
174                 tasks |= UpdateTask.Config;
175
176             await this.RunUpdateTasks(tasks, now).ConfigureAwait(false);
177         }
178
179         private async Task TimerCallback_AfterSystemResume()
180         {
181             // systemResumeMode では一定期間経過後に全てのタイムラインを更新する
182             var now = DateTimeUtc.Now;
183
184             var nextScheduledUpdateAll = this.SystemResumedAt + this.UpdateAfterSystemResume;
185             if (nextScheduledUpdateAll - now < TimeSpan.Zero)
186             {
187                 this.systemResumeMode = false;
188                 await this.RunUpdateTasks(UpdateTask.All, now).ConfigureAwait(false);
189             }
190         }
191
192         private async Task RunUpdateTasks(UpdateTask tasks, DateTimeUtc now)
193         {
194             var updateTasks = new List<Task>(capacity: 7);
195
196             // LastUpdate* を次の時刻に更新してから Update* を実行すること
197             // (LastUpdate* が更新されずに Update* が例外を投げると無限ループに陥る)
198
199             if ((tasks & UpdateTask.Home) == UpdateTask.Home)
200             {
201                 this.LastUpdateHome = now;
202                 if (this.UpdateHome != null)
203                     updateTasks.Add(this.UpdateHome());
204             }
205
206             if ((tasks & UpdateTask.Mention) == UpdateTask.Mention)
207             {
208                 this.LastUpdateMention = now;
209                 if (this.UpdateMention != null)
210                     updateTasks.Add(this.UpdateMention());
211             }
212
213             if ((tasks & UpdateTask.Dm) == UpdateTask.Dm)
214             {
215                 this.LastUpdateDm = now;
216                 if (this.UpdateDm != null)
217                     updateTasks.Add(this.UpdateDm());
218             }
219
220             if ((tasks & UpdateTask.PublicSearch) == UpdateTask.PublicSearch)
221             {
222                 this.LastUpdatePublicSearch = now;
223                 if (this.UpdatePublicSearch != null)
224                     updateTasks.Add(this.UpdatePublicSearch());
225             }
226
227             if ((tasks & UpdateTask.User) == UpdateTask.User)
228             {
229                 this.LastUpdateUser = now;
230                 if (this.UpdateUser != null)
231                     updateTasks.Add(this.UpdateUser());
232             }
233
234             if ((tasks & UpdateTask.List) == UpdateTask.List)
235             {
236                 this.LastUpdateList = now;
237                 if (this.UpdateList != null)
238                     updateTasks.Add(this.UpdateList());
239             }
240
241             if ((tasks & UpdateTask.Config) == UpdateTask.Config)
242             {
243                 this.LastUpdateConfig = now;
244                 if (this.UpdateConfig != null)
245                     updateTasks.Add(this.UpdateConfig());
246             }
247
248             await Task.WhenAll(updateTasks).ConfigureAwait(false);
249         }
250
251         private TimeSpan NextTimerDelay()
252         {
253             TimeSpan delay;
254
255             if (this.systemResumeMode)
256             {
257                 // systemResumeMode が有効な間は UpdateAfterSystemResume 以外の設定値を見ない
258                 var nextScheduledUpdateAll = this.SystemResumedAt + this.UpdateAfterSystemResume;
259                 delay = nextScheduledUpdateAll - DateTimeUtc.Now;
260
261                 return delay > TimeSpan.Zero ? delay : TimeSpan.Zero;
262             }
263
264             // 次に更新が予定される時刻を判定する
265             var min = DateTimeUtc.MaxValue;
266
267             var nextScheduledHome = this.LastUpdateHome + this.UpdateIntervalHome;
268             if (nextScheduledHome < min)
269                 min = nextScheduledHome;
270
271             var nextScheduledMention = this.LastUpdateMention + this.UpdateIntervalMention;
272             if (nextScheduledMention < min)
273                 min = nextScheduledMention;
274
275             var nextScheduledDm = this.LastUpdateDm + this.UpdateIntervalDm;
276             if (nextScheduledDm < min)
277                 min = nextScheduledDm;
278
279             var nextScheduledPublicSearch = this.LastUpdatePublicSearch + this.UpdateIntervalPublicSearch;
280             if (nextScheduledPublicSearch < min)
281                 min = nextScheduledPublicSearch;
282
283             var nextScheduledUser = this.LastUpdateUser + this.UpdateIntervalUser;
284             if (nextScheduledUser < min)
285                 min = nextScheduledUser;
286
287             var nextScheduledList = this.LastUpdateList + this.UpdateIntervalList;
288             if (nextScheduledList < min)
289                 min = nextScheduledList;
290
291             var nextScheduledConfig = this.LastUpdateConfig + this.UpdateIntervalConfig;
292             if (nextScheduledConfig < min)
293                 min = nextScheduledConfig;
294
295             delay = min - DateTimeUtc.Now;
296
297             return delay > TimeSpan.Zero ? delay : TimeSpan.Zero;
298         }
299     }
300 }