Make sure your asynchronous operations show up to work on time
$ dotnet add package Punchclock
Punchclock is the low-level scheduling and prioritization library used by Fusillade to orchestrate pending concurrent operations.
Ok, so you've got a shiny mobile phone app and you've got async/await. Awesome! It's so easy to issue network requests, why not do it all the time? After your users one-:star2: you for your app being slow, you discover that you're issuing way too many requests at the same time.
Then, you try to manage issuing less requests by hand, and it becomes a spaghetti mess as different parts of your app reach into each other to try to figure out who's doing what. Let's figure out a better way.
dotnet add package Punchclockusing Punchclock;
using System.Net.Http;
var queue = new OperationQueue(maximumConcurrent: 2);
var http = new HttpClient();
// Fire a bunch of downloads – only two will run at a time
var t1 = queue.Enqueue(1, () => http.GetStringAsync("https://example.com/a"));
var t2 = queue.Enqueue(1, () => http.GetStringAsync("https://example.com/b"));
var t3 = queue.Enqueue(1, () => http.GetStringAsync("https://example.com/c"));
await Task.WhenAll(t1, t2, t3);
await queue.Enqueue(10, () => http.GetStringAsync("https://example.com/urgent"));
// These will run one-after-another because they share the same key
var k1 = queue.Enqueue(1, key: "user:42", () => LoadUserAsync(42));
var k2 = queue.Enqueue(1, key: "user:42", () => LoadUserPostsAsync(42));
await Task.WhenAll(k1, k2);
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
await queue.Enqueue(1, key: "img:1", () => DownloadImageAsync("/1"), cts.Token);
var cancel = new Subject<Unit>();
var obs = queue.EnqueueObservableOperation(1, "slow", cancel, () => Expensive().ToObservable());
cancel.OnNext(Unit.Default); // cancels if not yet running or in-flight
var gate = queue.PauseQueue();
// enqueue work while paused; nothing executes yet
// ...
gate.Dispose(); // resumes and drains respecting priority/keys
queue.SetMaximumConcurrent(8); // increases throughput
await queue.ShutdownQueue(); // completes when outstanding work finishes
OperationQueue
OperationQueueExtensions
var queue = new OperationQueue(3);
Task Download(string url, string dest, int pri, string key) =>
queue.Enqueue(pri, key, async () =>
{
using var http = new HttpClient();
var bytes = await http.GetByteArrayAsync(url);
await File.WriteAllBytesAsync(dest, bytes);
});
var tasks = new[]
{
Download("https://example.com/a.jpg", "a.jpg", 1, "img"),
Download("https://example.com/b.jpg", "b.jpg", 1, "img"),
queue.Enqueue(5, () => Task.Delay(100)), // higher priority misc work
};
await Task.WhenAll(tasks);
Punchclock is developed under an OSI-approved open source license, making it freely usable and distributable, even for commercial use. Because of our Open Collective model for funding and transparency, we are able to funnel support and funds through to our contributors and community. We ❤ the people who are involved in this project, and we’d love to have you on board, especially if you are just getting started or have never contributed to open-source before.
So here's to you, lovely person who wants to join us — this is how you can support us: