-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathBackgroundQueueService.cs
More file actions
157 lines (136 loc) · 4.34 KB
/
Copy pathBackgroundQueueService.cs
File metadata and controls
157 lines (136 loc) · 4.34 KB
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
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Zapto.Mediator.Options;
namespace Zapto.Mediator.Services;
public class BackgroundQueueService
{
private readonly IOptions<MediatorBackgroundOptions> _options;
private readonly ConcurrentQueue<Func<Task>> _workItems = new();
private readonly List<Worker> _workers = new();
private readonly ILogger<BackgroundQueueService>? _logger;
private readonly IHostApplicationLifetime? _applicationLifetime;
public BackgroundQueueService(IHostApplicationLifetime? applicationLifetime = null, ILogger<BackgroundQueueService>? logger = null, IOptions<MediatorBackgroundOptions>? options = null)
{
_options = options ?? new OptionsWrapper<MediatorBackgroundOptions>(new MediatorBackgroundOptions());
_applicationLifetime = applicationLifetime;
_logger = logger;
}
public void QueueBackgroundWorkItem(Func<Task> workItem, object notification)
{
if (_applicationLifetime is { ApplicationStopping.IsCancellationRequested: true } &&
!_options.Value.AllowBackgroundWorkWhileStopping)
{
throw new OperationCanceledException("Cannot schedule work item since the application is stopping");
}
lock (_workers)
{
_workItems.Enqueue(workItem);
if (_workers.Count < _options.Value.MaxDegreeOfParallelism)
{
var worker = new Worker
{
Notification = notification
};
worker.Task = Task.Factory.StartNew(
() => ProcessBackgroundWorkItem(worker),
CancellationToken.None,
TaskCreationOptions.None,
TaskScheduler.Default).Unwrap();
_workers.Add(worker);
}
}
}
public object[] GetRunningNotifications()
{
lock (_workers)
{
return _workers.Select(w => w.Notification).ToArray();
}
}
public async Task WaitForBackgroundTasksAsync(CancellationToken cancellationToken)
{
#if !NET
var tcs = new TaskCompletionSource<bool>();
using var registration = cancellationToken.Register(() => tcs.TrySetResult(true));
Task? resultingTask = null;
#endif
while (true)
{
Task task;
lock (_workers)
{
if (_workers.Count == 0)
{
break;
}
task = _workers[0].Task;
}
try
{
#if NET
await task.WaitAsync(cancellationToken);
#else
resultingTask = await Task.WhenAny(task, tcs.Task);
#endif
}
catch (OperationCanceledException)
{
throw;
}
catch
{
// worker exceptions are already logged at the source; ignore here
}
#if !NET
if (resultingTask == tcs.Task)
{
throw new OperationCanceledException();
}
#endif
}
}
private async Task ProcessBackgroundWorkItem(Worker worker)
{
while (true)
{
// Check if there are no more work items and remove worker
if (_workItems.IsEmpty)
{
lock (_workers)
{
if (_workItems.IsEmpty)
{
_workers.Remove(worker);
break;
}
}
}
// Process next work item
if (!_workItems.TryDequeue(out var workItem))
{
await Task.Yield();
continue;
}
try
{
await workItem();
}
catch (Exception ex)
{
_logger?.LogError(ex, "Error occurred executing background notification");
}
}
}
private class Worker
{
public Task Task { get; set; } = Task.CompletedTask;
public object Notification { get; set; } = null!;
}
}