Skip to content

Commit 86e3ae9

Browse files
committed
🦙 Move long polling to a separate task
1 parent 90c2977 commit 86e3ae9

File tree

7 files changed

+178
-77
lines changed

7 files changed

+178
-77
lines changed

CubicBot.Telegram.App/CubicBot.Telegram.App.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
<ItemGroup>
1515
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
16+
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.0" />
1617
</ItemGroup>
1718

1819
<ItemGroup>
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
using Telegram.Bot;
2+
using Telegram.Bot.Types;
3+
using Telegram.Bot.Types.Enums;
4+
5+
namespace CubicBot.Telegram.App;
6+
7+
public sealed partial class LongPollingBotService(ILogger<LongPollingBotService> logger, IHttpClientFactory httpClientFactory) : BotService(logger, httpClientFactory.CreateClient())
8+
{
9+
private Task? _pollUpdatesTask;
10+
11+
public override async Task StartAsync(CancellationToken cancellationToken)
12+
{
13+
(TelegramBotClient bot, CancellationTokenSource cts) = await StartBotAsync(cancellationToken);
14+
_pollUpdatesTask = PollUpdatesAsync(bot, cts.Token);
15+
}
16+
17+
private async Task PollUpdatesAsync(ITelegramBotClient botClient, CancellationToken cancellationToken = default)
18+
{
19+
int? offset = null;
20+
21+
while (!cancellationToken.IsCancellationRequested)
22+
{
23+
try
24+
{
25+
Update[] updates = await botClient.GetUpdates(offset, allowedUpdates: [UpdateType.Message], cancellationToken: cancellationToken);
26+
27+
if (updates.Length > 0)
28+
{
29+
offset = updates[^1].Id + 1;
30+
31+
foreach (Update update in updates)
32+
{
33+
await UpdateWriter.WriteAsync(update, cancellationToken);
34+
}
35+
}
36+
}
37+
catch (OperationCanceledException)
38+
{
39+
return;
40+
}
41+
catch (Exception ex)
42+
{
43+
LogFailedToGetUpdates(ex);
44+
45+
try
46+
{
47+
await Task.Delay(s_delayOnError, cancellationToken);
48+
}
49+
catch (TaskCanceledException)
50+
{
51+
return;
52+
}
53+
}
54+
}
55+
}
56+
57+
private static readonly TimeSpan s_delayOnError = TimeSpan.FromSeconds(5);
58+
59+
[LoggerMessage(Level = LogLevel.Warning, Message = "Failed to get updates")]
60+
private partial void LogFailedToGetUpdates(Exception ex);
61+
62+
public override async Task StopAsync(CancellationToken cancellationToken)
63+
{
64+
if (Cts is null)
65+
{
66+
return;
67+
}
68+
69+
try
70+
{
71+
await base.StopAsync(cancellationToken);
72+
}
73+
finally
74+
{
75+
if (_pollUpdatesTask is not null)
76+
{
77+
await _pollUpdatesTask.WaitAsync(cancellationToken).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
78+
}
79+
}
80+
}
81+
}

CubicBot.Telegram.App/Program.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
using CubicBot.Telegram;
1+
using CubicBot.Telegram.App;
22

33
var builder = Host.CreateApplicationBuilder(args);
4-
builder.Services.AddHostedService<BotService>();
4+
builder.Services.AddHttpClient();
5+
builder.Services.AddHostedService<LongPollingBotService>();
56

67
var host = builder.Build();
78
host.Run();

CubicBot.Telegram.App/appsettings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
"Logging": {
33
"LogLevel": {
44
"Default": "Information",
5-
"Microsoft.Hosting.Lifetime": "Information"
5+
"Microsoft.Hosting.Lifetime": "Information",
6+
"System.Net.Http.HttpClient.Default": "Warning"
67
}
78
}
89
}

CubicBot.Telegram.App/appsettings.systemd.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
"Logging": {
33
"LogLevel": {
44
"Default": "Information",
5-
"Microsoft.Hosting.Lifetime": "Information"
5+
"Microsoft.Hosting.Lifetime": "Information",
6+
"System.Net.Http.HttpClient.Default": "Warning"
67
}
78
},
89
"Console": {

CubicBot.Telegram/BotService.cs

Lines changed: 65 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,39 @@
11
using Microsoft.Extensions.Hosting;
22
using Microsoft.Extensions.Logging;
3+
using System.Threading.Channels;
34
using Telegram.Bot;
45
using Telegram.Bot.Exceptions;
56
using Telegram.Bot.Types;
67

78
namespace CubicBot.Telegram;
89

9-
public sealed partial class BotService(ILogger<BotService> logger) : BackgroundService
10+
public partial class BotService : IHostedService
1011
{
11-
protected override Task ExecuteAsync(CancellationToken stoppingToken) => RunBotAsync(stoppingToken);
12+
private readonly ILogger<BotService> _logger;
13+
private readonly HttpClient _httpClient;
14+
private readonly ChannelReader<Update> _updateReader;
1215

13-
private async Task RunBotAsync(CancellationToken cancellationToken = default)
16+
public BotService(ILogger<BotService> logger, HttpClient httpClient)
17+
{
18+
_logger = logger;
19+
_httpClient = httpClient;
20+
Channel<Update> updateChannel = Channel.CreateBounded<Update>(new BoundedChannelOptions(64)
21+
{
22+
SingleReader = true,
23+
});
24+
_updateReader = updateChannel.Reader;
25+
UpdateWriter = updateChannel.Writer;
26+
}
27+
28+
public ChannelWriter<Update> UpdateWriter { get; }
29+
30+
protected CancellationTokenSource? Cts { get; private set; }
31+
private Task? _saveDataTask;
32+
private Task? _handleUpdatesTask;
33+
34+
public virtual Task StartAsync(CancellationToken cancellationToken) => StartBotAsync(cancellationToken);
35+
36+
protected async Task<(TelegramBotClient, CancellationTokenSource)> StartBotAsync(CancellationToken cancellationToken = default)
1437
{
1538
Config config;
1639

@@ -45,8 +68,7 @@ private async Task RunBotAsync(CancellationToken cancellationToken = default)
4568
{
4669
RetryCount = 7,
4770
};
48-
using HttpClient httpClient = new();
49-
TelegramBotClient bot = new(options, httpClient);
71+
TelegramBotClient bot = new(options, _httpClient);
5072
User me;
5173

5274
while (true)
@@ -58,47 +80,43 @@ private async Task RunBotAsync(CancellationToken cancellationToken = default)
5880
}
5981
catch (RequestException ex)
6082
{
61-
logger.LogWarning(ex, "Failed to get bot info, retrying in 30 seconds");
62-
await Task.Delay(30000, cancellationToken);
83+
_logger.LogWarning(ex, "Failed to get bot info, retrying in 30 seconds");
84+
await Task.Delay(s_delayOnError, cancellationToken);
6385
}
6486
}
6587

6688
if (string.IsNullOrEmpty(me.Username))
6789
throw new Exception("Bot username is null or empty.");
6890

69-
var updateHandler = new UpdateHandler(me.Username, config, data, logger);
91+
var updateHandler = new UpdateHandler(me.Username, config, data, _logger, bot);
7092

7193
while (true)
7294
{
7395
try
7496
{
75-
await updateHandler.RegisterCommandsAsync(bot, cancellationToken);
97+
await updateHandler.RegisterCommandsAsync(cancellationToken);
7698
break;
7799
}
78100
catch (RequestException ex)
79101
{
80-
logger.LogWarning(ex, "Failed to register commands, retrying in 30 seconds");
81-
await Task.Delay(30000, cancellationToken);
102+
_logger.LogWarning(ex, "Failed to register commands, retrying in 30 seconds");
103+
await Task.Delay(s_delayOnError, cancellationToken);
82104
}
83105
}
84106

85107
LogStartedBot(me.Username, me.Id);
86108

87-
var saveDataTask = SaveDataHourlyAsync(data, cancellationToken);
109+
Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
110+
_saveDataTask = SaveDataHourlyAsync(data, Cts.Token);
111+
_handleUpdatesTask = updateHandler.RunAsync(_updateReader, Cts.Token);
88112

89-
try
90-
{
91-
await updateHandler.RunAsync(bot, cancellationToken);
92-
}
93-
finally
94-
{
95-
await saveDataTask;
96-
}
113+
return (bot, Cts);
97114
}
98115

99116
[LoggerMessage(Level = LogLevel.Information, Message = "Started Telegram bot: @{BotUsername} ({BotId})")]
100117
private partial void LogStartedBot(string botUsername, long botId);
101118

119+
private static readonly TimeSpan s_delayOnError = TimeSpan.FromSeconds(30);
102120
private static readonly TimeSpan s_saveDataInterval = TimeSpan.FromHours(1);
103121

104122
private async Task SaveDataHourlyAsync(Data data, CancellationToken cancellationToken = default)
@@ -116,11 +134,36 @@ private async Task SaveDataHourlyAsync(Data data, CancellationToken cancellation
116134
try
117135
{
118136
await data.SaveAsync(CancellationToken.None);
119-
logger.LogDebug("Saved data");
137+
_logger.LogDebug("Saved data");
120138
}
121139
catch (Exception ex)
122140
{
123-
logger.LogError(ex, "Failed to save data");
141+
_logger.LogError(ex, "Failed to save data");
142+
}
143+
}
144+
}
145+
146+
public virtual async Task StopAsync(CancellationToken cancellationToken)
147+
{
148+
if (Cts is null)
149+
{
150+
return;
151+
}
152+
153+
try
154+
{
155+
Cts.Cancel();
156+
}
157+
finally
158+
{
159+
if (_saveDataTask is not null)
160+
{
161+
await _saveDataTask.WaitAsync(cancellationToken).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
162+
}
163+
164+
if (_handleUpdatesTask is not null)
165+
{
166+
await _handleUpdatesTask.WaitAsync(cancellationToken).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
124167
}
125168
}
126169
}

0 commit comments

Comments
 (0)