-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathProgramEntry.cs
167 lines (137 loc) · 6.37 KB
/
ProgramEntry.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
using System.Collections.Immutable;
using Microsoft.EntityFrameworkCore;
using Quartz;
using Quartz.Impl;
using Trawler.Common.Utility.Logging;
using Trawler.Config;
using Trawler.Database;
using Trawler.Database.Model;
using Trawler.Scheduler.Job;
using Trawler.Scheduler.Util;
namespace Trawler {
public static class ProgramEntry {
private static readonly LoggerBase logger = LoggerFactory.CreateLogger(subject: nameof(ProgramEntry));
private const string WaitHandleName = "04D477C6-6CB7-4375-AAA8-BC6C490F5889";
public static async Task Main(string[] args) {
logger.Log("Program started.");
await Initialize();
await AfterInitialization();
await StartScheduler();
await KeepRunning();
}
private static async Task Initialize() {
logger.Log("Start initialization...");
// *** Configuration *** //
try {
await Configuration.Instance.Load();
} catch {
logger.LogError("Configuration loading failed.");
throw;
}
// *** Database connection test *** //
if(!await DatabaseContext.TestConnection()) {
logger.LogError("Database connection should be configured and available to use this program.");
throw new ApplicationException("Database connection should be configured and available to use this program.");
}
logger.Log("Initialization completed.");
}
private static async Task AfterInitialization() {
logger.Log("Starting after initialization tasks...");
logger.Log("* Acquire post creation date for CrawlType.SinglePost targets if needed");
await TwitterSinglePostCrawlJob.AcquirePostCreationDateForTargets();
logger.Log("After initialization tasks completed.");
}
private static async Task StartScheduler() {
logger.Log("Starting scheduler...");
TimeZoneInfo timezone = TimeZoneInfo.FindSystemTimeZoneById(Configuration.Instance.Config.Scheduler.DefaultTimezone);
StdSchedulerFactory schedulerFactory = new StdSchedulerFactory();
IScheduler scheduler = await schedulerFactory.GetScheduler();
await scheduler.Start();
ITrigger dailyTrigger = TriggerBuilder.Create()
.WithIdentity("DailyTrigger", "Crawler")
.StartNow()
.WithCronSchedule("0 50 23 * * ?", x => x.InTimeZone(timezone))
.Build();
ITrigger immediateTrigger = TriggerBuilder.Create()
.WithIdentity("ImmediateTrigger", "Crawler")
.StartNow()
.Build();
// Twitter account data crawl job - daily
{
IJobDetail accountDataJobDetail = JobBuilder.Create<TwitterAccountDataCrawlJob>()
.WithIdentity("TwitterAccountData", "Crawler")
.Build();
await scheduler.ScheduleJob(accountDataJobDetail, dailyTrigger);
if(await scheduler.CheckExists(accountDataJobDetail.Key)) {
logger.Log("Daily Twitter account data crawl job scheduled.");
if(dailyTrigger.GetNextFireTimeUtc() is { UtcDateTime: { } nextFireTimeUtcDateTime }) {
logger.Log($" \u2514 Next run at UTC: {nextFireTimeUtcDateTime}");
logger.Log($" \u2514 Next run at {timezone.Id}: {TimeZoneInfo.ConvertTimeFromUtc(nextFireTimeUtcDateTime, timezone)}");
}
}
}
// Twitter single post crawl job - per target
List<IJobDetail> singlePostJobDetails = [];
{
await using DatabaseContext db = new();
CrawlTarget[] targets = await db.CrawlTargets
.Where(x => x.CrawlType == CrawlTargetType.SinglePost)
.ToArrayAsync();
foreach(CrawlTarget target in targets) {
IJobDetail jobDetail = JobBuilder.Create<TwitterSinglePostCrawlJob>()
.WithIdentity($"TwitterSinglePost_{target.TargetScreenName}_{target.TargetId}", "Crawler")
.Build();
jobDetail.JobDataMap.Put("targetDbId", target.Id);
ImmutableArray<ITrigger> triggers = TwitterSinglePostCrawlTriggerBuilder.Build(target, jobDetail);
if(triggers.Length <= 0) {
logger.Log($"Single post target {target.TargetScreenName}/{target.TargetId} has expired and will not be scheduled.");
continue;
}
await scheduler.AddJob(jobDetail, replace: true, storeNonDurableWhileAwaitingScheduling: true);
logger.Log($"Scheduling Twitter single post crawl job for {target.TargetScreenName}/{target.TargetId}");
foreach(ITrigger trigger in triggers) {
await scheduler.ScheduleJob(trigger);
logger.Log($" \u2514 {trigger.GetNextFireTimeUtc()?.ToString()}");
}
if(await scheduler.CheckExists(jobDetail.Key)) {
logger.Log($"Twitter single post crawl job scheduled for {target.TargetScreenName}/{target.TargetId}");
singlePostJobDetails.Add(jobDetail);
}
}
if(singlePostJobDetails.Count > 0) {
logger.Log($"Total {singlePostJobDetails.Count} Twitter single post crawl job(s) scheduled.");
}
}
}
private static async Task KeepRunning() {
bool signaled = false;
bool createdNew = false;
// Setup EventWaitHandle
EventWaitHandle waitHandle;
try {
waitHandle = new EventWaitHandle(false, EventResetMode.AutoReset, WaitHandleName,
out createdNew);
} catch(PlatformNotSupportedException) {
logger.LogWarning("Platform does not support named EventWaitHandle. Fallback to unnamed EventWaitHandle.");
waitHandle = new EventWaitHandle(false, EventResetMode.AutoReset, null, out createdNew);
} catch(Exception e) {
logger.LogError("Failed to create EventWaitHandle. Program will exit.", e);
return;
}
// Check if another instance is already running
if(!createdNew) {
logger.LogError("Another instance of this program is already running. This instance will exit.");
waitHandle.Set();
return;
}
// Wait forever until exit signal is received
logger.Log("Program is now keeping running.");
await using var timer = new Timer(_ => { }, null, TimeSpan.Zero, TimeSpan.FromSeconds(60));
do {
signaled = waitHandle.WaitOne(TimeSpan.FromSeconds(10));
} while(!signaled);
// Program will exit after signal
logger.Log("Got signal to exit. Program will exit.");
}
}
}