{"id":13679939,"url":"https://github.com/reactiveui/punchclock","last_synced_at":"2025-11-30T15:05:01.002Z","repository":{"id":630171,"uuid":"13405279","full_name":"reactiveui/punchclock","owner":"reactiveui","description":"Make sure your asynchronous operations show up to work on time","archived":false,"fork":false,"pushed_at":"2025-04-09T20:14:09.000Z","size":4791,"stargazers_count":261,"open_issues_count":3,"forks_count":21,"subscribers_count":17,"default_branch":"main","last_synced_at":"2025-04-09T21:25:08.951Z","etag":null,"topics":["async","cross-platform","dotnet","dotnet-core","http-client","xamarin"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/reactiveui.png","metadata":{"funding":{"github":["reactivemarbles"]},"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2013-10-08T06:29:40.000Z","updated_at":"2025-04-09T20:14:11.000Z","dependencies_parsed_at":"2023-07-06T18:01:29.067Z","dependency_job_id":"b7e88913-abd1-47bf-ac5d-21d787d39a14","html_url":"https://github.com/reactiveui/punchclock","commit_stats":{"total_commits":329,"total_committers":20,"mean_commits":16.45,"dds":0.6170212765957447,"last_synced_commit":"33b0898d4bef42d290a6f2e5b7f4ec73c3249161"},"previous_names":["paulcbetts/punchclock"],"tags_count":16,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/reactiveui%2Fpunchclock","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/reactiveui%2Fpunchclock/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/reactiveui%2Fpunchclock/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/reactiveui%2Fpunchclock/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/reactiveui","download_url":"https://codeload.github.com/reactiveui/punchclock/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248657860,"owners_count":21140843,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["async","cross-platform","dotnet","dotnet-core","http-client","xamarin"],"created_at":"2024-08-02T13:01:11.266Z","updated_at":"2025-11-30T15:05:00.996Z","avatar_url":"https://github.com/reactiveui.png","language":"C#","funding_links":["https://github.com/sponsors/reactivemarbles"],"categories":["C# #"],"sub_categories":[],"readme":"[![NuGet Stats](https://img.shields.io/nuget/v/punchclock.svg)](https://www.nuget.org/packages/punchclock) ![Build](https://github.com/reactiveui/punchclock/workflows/Build/badge.svg)\r\n [![Code Coverage](https://codecov.io/gh/reactiveui/punchclock/branch/main/graph/badge.svg)](https://codecov.io/gh/reactiveui/punchclock) [![#yourfirstpr](https://img.shields.io/badge/first--timers--only-friendly-blue.svg)](https://reactiveui.net/contribute) \r\n\u003cbr\u003e\r\n\r\n\u003cbr /\u003e\r\n\u003ca href=\"https://github.com/reactiveui/punchclock\"\u003e\r\n  \u003cimg width=\"120\" heigth=\"120\" src=\"https://raw.githubusercontent.com/reactiveui/styleguide/master/logo_punchclock/main.png\"\u003e\r\n\u003c/a\u003e\r\n\r\n## Punchclock: A library for managing concurrent operations\r\n\r\nPunchclock is the low-level scheduling and prioritization library used by\r\n[Fusillade](https://github.com/reactiveui/Fusillade) to orchestrate pending\r\nconcurrent operations.\r\n\r\n### What even does that mean?\r\n\r\nOk, so you've got a shiny mobile phone app and you've got async/await.\r\nAwesome! It's so easy to issue network requests, why not do it all the time?\r\nAfter your users one-:star2: you for your app being slow, you discover that\r\nyou're issuing *way* too many requests at the same time. \r\n\r\nThen, you try to manage issuing less requests by hand, and it becomes a\r\nspaghetti mess as different parts of your app reach into each other to try to\r\nfigure out who's doing what. Let's figure out a better way.\r\n\r\n## Key features\r\n\r\n- Bounded concurrency with a priority-aware semaphore\r\n- Priority scheduling (higher number runs first)\r\n- Key-based serialization (only one operation per key runs at a time)\r\n- Pause/resume with reference counting\r\n- Cancellation via CancellationToken or IObservable\r\n- Task and IObservable friendly API\r\n\r\n## Install\r\n\r\n- NuGet: `dotnet add package Punchclock`\r\n\r\n## Quick start\r\n\r\n```csharp\r\nusing Punchclock;\r\nusing System.Net.Http;\r\n\r\nvar queue = new OperationQueue(maximumConcurrent: 2);\r\nvar http = new HttpClient();\r\n\r\n// Fire a bunch of downloads – only two will run at a time\r\nvar t1 = queue.Enqueue(1, () =\u003e http.GetStringAsync(\"https://example.com/a\"));\r\nvar t2 = queue.Enqueue(1, () =\u003e http.GetStringAsync(\"https://example.com/b\"));\r\nvar t3 = queue.Enqueue(1, () =\u003e http.GetStringAsync(\"https://example.com/c\"));\r\nawait Task.WhenAll(t1, t2, t3);\r\n```\r\n\r\n## Priorities\r\n\r\n- Higher numbers win. A priority 10 operation will preempt priority 1 when a slot opens.\r\n\r\n```csharp\r\nawait queue.Enqueue(10, () =\u003e http.GetStringAsync(\"https://example.com/urgent\"));\r\n```\r\n\r\n## Keys: serialize related work\r\n\r\n- Use a key to ensure only one operation for that key runs at a time.\r\n- Useful to avoid thundering herds against the same resource.\r\n\r\n```csharp\r\n// These will run one-after-another because they share the same key\r\nvar k1 = queue.Enqueue(1, key: \"user:42\", () =\u003e LoadUserAsync(42));\r\nvar k2 = queue.Enqueue(1, key: \"user:42\", () =\u003e LoadUserPostsAsync(42));\r\nawait Task.WhenAll(k1, k2);\r\n```\r\n\r\n## Cancellation\r\n\r\n- Via CancellationToken:\r\n\r\n```csharp\r\nusing var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));\r\nawait queue.Enqueue(1, key: \"img:1\", () =\u003e DownloadImageAsync(\"/1\"), cts.Token);\r\n```\r\n\r\n- Via IObservable cancellation signal:\r\n\r\n```csharp\r\nvar cancel = new Subject\u003cUnit\u003e();\r\nvar obs = queue.EnqueueObservableOperation(1, \"slow\", cancel, () =\u003e Expensive().ToObservable());\r\ncancel.OnNext(Unit.Default); // cancels if not yet running or in-flight\r\n```\r\n\r\n## Pause and resume\r\n\r\n```csharp\r\nvar gate = queue.PauseQueue();\r\n// enqueue work while paused; nothing executes yet\r\n// ...\r\ngate.Dispose(); // resumes and drains respecting priority/keys\r\n```\r\n\r\n## Adjust concurrency at runtime\r\n\r\n```csharp\r\nqueue.SetMaximumConcurrent(8); // increases throughput\r\n```\r\n\r\n## Shutting down\r\n\r\n```csharp\r\nawait queue.ShutdownQueue(); // completes when outstanding work finishes\r\n```\r\n\r\n## API overview\r\n\r\n- OperationQueue\r\n  - ctor(int maximumConcurrent = 4)\r\n  - IObservable\u003cT\u003e EnqueueObservableOperation\u003cT\u003e(int priority, Func\u003cIObservable\u003cT\u003e\u003e)\r\n  - IObservable\u003cT\u003e EnqueueObservableOperation\u003cT\u003e(int priority, string key, Func\u003cIObservable\u003cT\u003e\u003e)\r\n  - IObservable\u003cT\u003e EnqueueObservableOperation\u003cT, TDontCare\u003e(int priority, string key, IObservable\u003cTDontCare\u003e cancel, Func\u003cIObservable\u003cT\u003e\u003e)\r\n  - IDisposable PauseQueue()\r\n  - void SetMaximumConcurrent(int maximumConcurrent)\r\n  - IObservable\u003cUnit\u003e ShutdownQueue()\r\n\r\n- OperationQueueExtensions\r\n  - Task Enqueue(int priority, Func\u003cTask\u003e)\r\n  - Task\u003cT\u003e Enqueue\u003cT\u003e(int priority, Func\u003cTask\u003cT\u003e\u003e)\r\n  - Task Enqueue(int priority, string key, Func\u003cTask\u003e)\r\n  - Task\u003cT\u003e Enqueue\u003cT\u003e(int priority, string key, Func\u003cTask\u003cT\u003e\u003e)\r\n  - Overloads with CancellationToken for all of the above\r\n\r\n## Best practices\r\n\r\n- Prefer Task-based Enqueue APIs in application code; use observable APIs when composing with Rx.\r\n- Use descriptive keys for shared resources (e.g., \"user:{id}\", \"file:{path}\").\r\n- Keep operations idempotent and short; long operations block concurrency slots.\r\n- Use higher priorities sparingly; they jump the queue when a slot opens.\r\n- PauseQueue is ref-counted; always dispose the returned handle exactly once.\r\n- For cancellation via token, reuse CTS per user action to cancel pending work quickly.\r\n\r\n## Advanced notes\r\n\r\n- Unkeyed work is prioritized ahead of keyed work internally to keep the pipeline flowing; keys are serialized per group.\r\n- The semaphore releases when an operation completes, errors, or is canceled.\r\n- Cancellation before evaluation prevents invoking the supplied function.\r\n\r\n## Full examples\r\n\r\n- Image downloader with keys and priorities\r\n\r\n```csharp\r\nvar queue = new OperationQueue(3);\r\n\r\nTask Download(string url, string dest, int pri, string key) =\u003e\r\n    queue.Enqueue(pri, key, async () =\u003e\r\n    {\r\n        using var http = new HttpClient();\r\n        var bytes = await http.GetByteArrayAsync(url);\r\n        await File.WriteAllBytesAsync(dest, bytes);\r\n    });\r\n\r\nvar tasks = new[]\r\n{\r\n    Download(\"https://example.com/a.jpg\", \"a.jpg\", 1, \"img\"),\r\n    Download(\"https://example.com/b.jpg\", \"b.jpg\", 1, \"img\"),\r\n    queue.Enqueue(5, () =\u003e Task.Delay(100)), // higher priority misc work\r\n};\r\nawait Task.WhenAll(tasks);\r\n```\r\n\r\n## Troubleshooting\r\n\r\n- Nothing runs? Ensure you didn't leave the queue paused. Dispose the token from PauseQueue.\r\n- Starvation? Check if you assigned very high priorities to long-running tasks.\r\n- Deadlock-like behavior with keys? Remember keyed operations are strictly serialized; avoid long critical sections.\r\n\r\n## Contribute\r\n\r\nPunchclock 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.\r\n\r\nSo here's to you, lovely person who wants to join us — this is how you can support us:\r\n\r\n- [Responding to questions on StackOverflow](https://stackoverflow.com/questions/tagged/punchclock)\r\n- [Passing on knowledge and teaching the next generation of developers](https://ericsink.com/entries/dont_use_rxui.html)\r\n- Submitting documentation updates where you see fit or lacking.\r\n- Making contributions to the code base.\r\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Freactiveui%2Fpunchclock","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Freactiveui%2Fpunchclock","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Freactiveui%2Fpunchclock/lists"}