Skip to content

Commit b40f5e3

Browse files
authored
Merge pull request #6 from digixoil/develop
Add work dispatching and related rate control support, in addition to mutual exclusion manager
2 parents d4cac98 + a02b99c commit b40f5e3

23 files changed

Lines changed: 4243 additions & 170 deletions

README.md

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ A lightweight, high-performance, thread-safe background work queue manager and d
1212
- 🔒 **Safe Background Execution**: No scoped service leaks across async boundaries
1313
- 🔄 **Context Capture & Rehydration**: Automatically captures and restores context (user claims, tenant ID, etc.)
1414
- ⚙️ **Flexible Handler Pipeline**: Multiple handlers can process the same work type with ordered execution
15+
- ⏱️ **Rate Control**: Built-in debounce and throttle scheduling for controlling work execution timing
1516
- 📊 **Observable**: Structured logging at all execution boundaries
1617
- 🎯 **Multi-Targeting**: Supports .NET 6.0 through .NET 10.0
1718
- 📚 **Fully Documented**: Comprehensive XML documentation for all public APIs
@@ -24,6 +25,7 @@ XpressWork provides safe, in-memory background work execution for .NET applicati
2425
- Request context (user identity, tenant, etc.) is captured and restored
2526
- Multiple handlers can process work items in deterministic order
2627
- Proper cancellation and exception handling
28+
- Rate-controlled scheduling with debounce and throttle patterns
2729

2830
## What XpressWork Is NOT
2931

@@ -174,23 +176,130 @@ await submitter.Enqueue(new BackgroundActionWorkArguments(
174176
Name: "ProcessOrder"));
175177
```
176178

179+
## Rate-Controlled Scheduling
180+
181+
XpressWork includes a scheduling layer for rate-controlling when work is submitted to the queue. This is useful for scenarios where you need to coalesce rapid events or limit execution frequency.
182+
183+
### Enable Scheduling
184+
185+
```csharp
186+
services.AddXpressWork<MyAppContext, MyAppScopeFactory>();
187+
services.AddXpressWorkScheduling<MyAppContext>();
188+
```
189+
190+
### Debounce Pattern
191+
192+
Execute work only after a period of inactivity. Useful for auto-save scenarios where you want to save only after the user stops typing.
193+
194+
```csharp
195+
public class PreferencesService
196+
{
197+
private readonly IBackgroundWorkScheduler<MyAppContext> _scheduler;
198+
199+
public PreferencesService(IBackgroundWorkScheduler<MyAppContext> scheduler)
200+
{
201+
_scheduler = scheduler;
202+
}
203+
204+
public async Task SavePreferencesAsync(Guid userId, UserPreferences prefs)
205+
{
206+
// Only save after 500ms of inactivity
207+
// Multiple rapid calls reset the timer
208+
await _scheduler.Debounce(
209+
interval: TimeSpan.FromMilliseconds(500),
210+
key: userId, // Unique key per user
211+
workArgument: new SavePreferencesArgs { UserId = userId, Preferences = prefs });
212+
}
213+
}
214+
```
215+
216+
### Throttle Pattern
217+
218+
Execute work at most once per interval. Useful for sensor data or metrics where you want regular updates regardless of input frequency.
219+
220+
```csharp
221+
public class SensorService
222+
{
223+
private readonly IBackgroundWorkScheduler<MyAppContext> _scheduler;
224+
225+
public async Task ProcessTemperatureAsync(Guid sensorId, double temperature)
226+
{
227+
// Process at most once per second, using the latest reading
228+
await _scheduler.Throttle(
229+
interval: TimeSpan.FromSeconds(1),
230+
key: sensorId,
231+
workArgument: new ProcessTemperatureArgs { SensorId = sensorId, Temperature = temperature });
232+
}
233+
}
234+
```
235+
236+
### Run-Once-Then Patterns
237+
238+
Execute immediately on first call, then apply rate limiting for subsequent calls.
239+
240+
```csharp
241+
// Execute first call immediately, then debounce subsequent calls
242+
await _scheduler.RunOnceThenDebounce(
243+
interval: TimeSpan.FromMilliseconds(500),
244+
key: userId,
245+
workArgument: new SaveArgs { ... });
246+
247+
// Execute first call immediately, then throttle subsequent calls
248+
await _scheduler.RunOnceThenThrottle(
249+
interval: TimeSpan.FromSeconds(1),
250+
key: sensorId,
251+
workArgument: new ProcessArgs { ... });
252+
```
253+
254+
### Cancellation and Flushing
255+
256+
```csharp
257+
// Cancel pending scheduled work
258+
var (action, parameters) = _scheduler.Cancel<SavePreferencesArgs>(userId);
259+
260+
// Flush all pending work immediately (also called on shutdown)
261+
await _scheduler.FlushAsync();
262+
263+
// Discard all pending work without execution
264+
_scheduler.Clear();
265+
```
266+
177267
## Architecture Overview
178268

179269
| Component | Lifetime | Responsibility |
180270
|-----------|----------|----------------|
181271
| `BackgroundWorkQueue<TContext>` | **Singleton** | Owns channel, scheduling, execution |
182272
| `IBackgroundWorkSubmitter<TContext>` | **Scoped** | Captures caller scope & enqueues |
273+
| `IBackgroundWorkScheduler<TContext>` | **Singleton** | Rate-controlled scheduling (debounce/throttle) |
183274
| `IBackgroundWorkScopeFactory<TContext>` | Singleton | Capture + rehydrate logic |
184275
| `IBackgroundWorkContextAccessor<TContext>` | Scoped | Holds rehydrated context |
185276
| `IWorkHandler<TArgs>` | Scoped | Executes work |
186277

278+
### Conceptual Architecture
279+
280+
```
281+
┌──────────────────────────────────────────────────────────────┐
282+
│ Your Application │
283+
├──────────────────────────────────────────────────────────────┤
284+
│ IBackgroundWorkScheduler │ IBackgroundWorkSubmitter │
285+
│ (rate-controlled scheduling) │ (immediate submission) │
286+
├──────────────────────────────────────────────────────────────┤
287+
│ IBackgroundWorkQueue │
288+
│ (execution pipeline) │
289+
├──────────────────────────────────────────────────────────────┤
290+
│ IWorkHandler<T> │ IWorkHandler<T> │ IWorkHandler<T> │
291+
│ (ordered execution in isolated scopes) │
292+
└──────────────────────────────────────────────────────────────┘
293+
```
294+
187295
### Key Invariants
188296

189297
1. **Background execution never uses the original request/caller scope**
190298
2. **No scoped services are captured and reused across async boundaries**
191299
3. **Each work item executes in a new `AsyncServiceScope`**
192300
4. **Context is rehydrated before resolving handlers**
193301
5. **Handler ordering is deterministic and stable**
302+
6. **Scheduler flushes all pending work on graceful shutdown**
194303

195304
## Configuration Options
196305

@@ -233,20 +342,24 @@ Handlers with the same `Order` value are sorted deterministically by type name.
233342
- Resolve services inside handlers, not in work arguments
234343
- Use the scoped submitter from within a DI scope
235344
- Handle cancellation tokens properly in handlers
345+
- Use debounce for user input that triggers saves/updates
346+
- Use throttle for high-frequency events (sensors, metrics)
236347

237348
### 🚫 DON'T
238349

239350
- Capture scoped services in work arguments
240351
- Store `IServiceProvider` or `HttpContext` in work arguments
241352
- Resolve the submitter from a singleton
242353
- Use XpressWork for work that requires persistence or distribution
354+
- Use very short debounce/throttle intervals (< 10ms)
243355

244356
## Thread Safety
245357

246358
All operations are thread-safe:
247359
- Multiple producers can enqueue concurrently
248360
- Context does not bleed across work items
249361
- Queue processing is sequential by default
362+
- Scheduler operations are thread-safe with concurrent dictionary storage
250363

251364
## Examples
252365

0 commit comments

Comments
 (0)