├── .gitattributes
├── .github
├── renovate.json
└── workflows
│ ├── ci-build.yml
│ ├── ci-buildAndRelease.yml
│ └── lock.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── LICENSE
├── README.md
├── images
└── logo.png
├── src
├── Directory.build.props
├── Directory.build.targets
├── Punchclock.Tests
│ ├── API
│ │ ├── ApiApprovalTests.PunchclockTests.DotNet6_0.verified.txt
│ │ ├── ApiApprovalTests.PunchclockTests.DotNet7_0.verified.txt
│ │ ├── ApiApprovalTests.PunchclockTests.DotNet8_0.verified.txt
│ │ ├── ApiApprovalTests.PunchclockTests.Net4_7.verified.txt
│ │ ├── ApiApprovalTests.cs
│ │ └── ApiExtensions.cs
│ ├── OperationQueueTests.cs
│ └── Punchclock.Tests.csproj
├── Punchclock.sln
├── Punchclock
│ ├── KeyedOperation.cs
│ ├── OperationQueue.cs
│ ├── OperationQueueExtensions.cs
│ ├── PriorityQueue.cs
│ ├── PrioritySemaphoreSubject.cs
│ ├── Punchclock.csproj
│ └── ScheduledSubject.cs
├── analyzers.ruleset
└── stylecop.json
└── version.json
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.doc diff=astextplain
2 | *.DOC diff=astextplain
3 | *.docx diff=astextplain
4 | *.DOCX diff=astextplain
5 | *.dot diff=astextplain
6 | *.DOT diff=astextplain
7 | *.pdf diff=astextplain
8 | *.PDF diff=astextplain
9 | *.rtf diff=astextplain
10 | *.RTF diff=astextplain
11 |
12 | *.jpg binary
13 | *.png binary
14 | *.gif binary
15 |
16 | *.cs text diff=csharp
17 | *.vb text
18 | *.c text
19 | *.cpp text
20 | *.cxx text
21 | *.h text
22 | *.hxx text
23 | *.py text
24 | *.rb text
25 | *.java text
26 | *.html text
27 | *.htm text
28 | *.css text
29 | *.scss text
30 | *.sass text
31 | *.less text
32 | *.js text
33 | *.lisp text
34 | *.clj text
35 | *.sql text
36 | *.php text
37 | *.lua text
38 | *.m text
39 | *.asm text
40 | *.erl text
41 | *.fs text
42 | *.fsx text
43 | *.hs text
44 |
45 | *.csproj text merge=union
46 | *.vbproj text merge=union
47 | *.fsproj text merge=union
48 | *.dbproj text merge=union
49 | *.sln text eol=crlf merge=union
50 |
51 |
--------------------------------------------------------------------------------
/.github/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": ["local>reactiveui/.github:renovate"]
4 | }
--------------------------------------------------------------------------------
/.github/workflows/ci-build.yml:
--------------------------------------------------------------------------------
1 | name: build
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | env:
10 | productNamespacePrefix: "Punchclock"
11 |
12 | permissions:
13 | contents: read
14 |
15 | jobs:
16 | build:
17 | permissions:
18 | contents: none
19 | uses: reactiveui/actions-common/.github/workflows/workflow-common-setup-and-build.yml@main
20 | with:
21 | configuration: Release
22 | productNamespacePrefix: "Punchclock"
23 | installWorkflows: false
24 |
--------------------------------------------------------------------------------
/.github/workflows/ci-buildAndRelease.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | - patches/*
8 |
9 | env:
10 | productNamespacePrefix: "Punchclock"
11 |
12 | permissions:
13 | contents: write
14 |
15 | jobs:
16 | release:
17 | uses: reactiveui/actions-common/.github/workflows/workflow-common-release.yml@main
18 | with:
19 | configuration: Release
20 | productNamespacePrefix: "Punchclock"
21 | installWorkflows: false
22 | secrets:
23 | SIGN_CLIENT_USER_ID: ${{ secrets.SIGN_CLIENT_USER_ID }}
24 | SIGN_CLIENT_SECRET: ${{ secrets.SIGN_CLIENT_SECRET }}
25 | SIGN_CLIENT_CONFIG: ${{ secrets.SIGN_CLIENT_CONFIG }}
26 | NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
27 |
--------------------------------------------------------------------------------
/.github/workflows/lock.yml:
--------------------------------------------------------------------------------
1 | name: 'Lock Threads'
2 |
3 | on:
4 | schedule:
5 | - cron: '0 0 * * *'
6 | workflow_dispatch:
7 |
8 | permissions:
9 | issues: write
10 | pull-requests: write
11 |
12 | concurrency:
13 | group: lock
14 |
15 | jobs:
16 | action:
17 | runs-on: ubuntu-latest
18 | steps:
19 | - uses: dessant/lock-threads@v5
20 | with:
21 | github-token: ${{ github.token }}
22 | issue-inactive-days: '14'
23 | pr-inactive-days: '14'
24 | issue-comment: >
25 | This issue has been automatically locked since there
26 | has not been any recent activity after it was closed.
27 | Please open a new issue for related bugs.
28 | pr-comment: >
29 | This pull request has been automatically locked since there
30 | has not been any recent activity after it was closed.
31 | Please open a new issue for related bugs.
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 |
4 | # User-specific files
5 | *.suo
6 | *.user
7 | *.userosscache
8 | *.sln.docstates
9 |
10 | # User-specific files (MonoDevelop/Xamarin Studio)
11 | *.userprefs
12 |
13 | # Build results
14 | [Dd]ebug/
15 | [Dd]ebugPublic/
16 | [Rr]elease/
17 | [Rr]eleases/
18 | x64/
19 | x86/
20 | bld/
21 | [Bb]in/
22 | [Oo]bj/
23 | [Ll]og/
24 | [Bb]uild/
25 | [Tt]ools/
26 |
27 | # Visual Studio 2015 cache/options directory
28 | .vs/
29 | # Uncomment if you have tasks that create the project's static files in wwwroot
30 | #wwwroot/
31 |
32 | # MSTest test Results
33 | [Tt]est[Rr]esult*/
34 | [Bb]uild[Ll]og.*
35 |
36 | # NUNIT
37 | *.VisualState.xml
38 | TestResult.xml
39 |
40 | # Build Results of an ATL Project
41 | [Dd]ebugPS/
42 | [Rr]eleasePS/
43 | dlldata.c
44 |
45 | # DNX
46 | project.lock.json
47 | project.fragment.lock.json
48 | artifacts/
49 |
50 | *_i.c
51 | *_p.c
52 | *_i.h
53 | *.ilk
54 | *.meta
55 | *.obj
56 | *.pch
57 | *.pdb
58 | *.pgc
59 | *.pgd
60 | *.rsp
61 | *.sbr
62 | *.tlb
63 | *.tli
64 | *.tlh
65 | *.tmp
66 | *.tmp_proj
67 | *.log
68 | *.vspscc
69 | *.vssscc
70 | .builds
71 | *.pidb
72 | *.svclog
73 | *.scc
74 |
75 | # Chutzpah Test files
76 | _Chutzpah*
77 |
78 | # Visual C++ cache files
79 | ipch/
80 | *.aps
81 | *.ncb
82 | *.opendb
83 | *.opensdf
84 | *.sdf
85 | *.cachefile
86 | *.VC.db
87 | *.VC.VC.opendb
88 |
89 | # Visual Studio profiler
90 | *.psess
91 | *.vsp
92 | *.vspx
93 | *.sap
94 |
95 | # TFS 2012 Local Workspace
96 | $tf/
97 |
98 | # Guidance Automation Toolkit
99 | *.gpState
100 |
101 | # ReSharper is a .NET coding add-in
102 | _ReSharper*/
103 | *.[Rr]e[Ss]harper
104 | *.DotSettings.user
105 |
106 | # JustCode is a .NET coding add-in
107 | .JustCode
108 |
109 | # TeamCity is a build add-in
110 | _TeamCity*
111 |
112 | # DotCover is a Code Coverage Tool
113 | *.dotCover
114 |
115 | # NCrunch
116 | _NCrunch_*
117 | .*crunch*.local.xml
118 | nCrunchTemp_*
119 |
120 | # MightyMoose
121 | *.mm.*
122 | AutoTest.Net/
123 |
124 | # Web workbench (sass)
125 | .sass-cache/
126 |
127 | # Installshield output folder
128 | [Ee]xpress/
129 |
130 | # DocProject is a documentation generator add-in
131 | DocProject/buildhelp/
132 | DocProject/Help/*.HxT
133 | DocProject/Help/*.HxC
134 | DocProject/Help/*.hhc
135 | DocProject/Help/*.hhk
136 | DocProject/Help/*.hhp
137 | DocProject/Help/Html2
138 | DocProject/Help/html
139 |
140 | # Click-Once directory
141 | publish/
142 |
143 | # Publish Web Output
144 | *.[Pp]ublish.xml
145 | *.azurePubxml
146 | # TODO: Comment the next line if you want to checkin your web deploy settings
147 | # but database connection strings (with potential passwords) will be unencrypted
148 | *.pubxml
149 | *.publishproj
150 |
151 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
152 | # checkin your Azure Web App publish settings, but sensitive information contained
153 | # in these scripts will be unencrypted
154 | PublishScripts/
155 |
156 | # NuGet Packages
157 | *.nupkg
158 | # The packages folder can be ignored because of Package Restore
159 | **/packages/*
160 | # except build/, which is used as an MSBuild target.
161 | !**/packages/build/
162 | # Uncomment if necessary however generally it will be regenerated when needed
163 | #!**/packages/repositories.config
164 | # NuGet v3's project.json files produces more ignoreable files
165 | *.nuget.props
166 | *.nuget.targets
167 |
168 | # Microsoft Azure Build Output
169 | csx/
170 | *.build.csdef
171 |
172 | # Microsoft Azure Emulator
173 | ecf/
174 | rcf/
175 |
176 | # Windows Store app package directories and files
177 | AppPackages/
178 | BundleArtifacts/
179 | Package.StoreAssociation.xml
180 | _pkginfo.txt
181 |
182 | # Visual Studio cache files
183 | # files ending in .cache can be ignored
184 | *.[Cc]ache
185 | # but keep track of directories ending in .cache
186 | !*.[Cc]ache/
187 |
188 | # Others
189 | ClientBin/
190 | ~$*
191 | *~
192 | *.dbmdl
193 | *.dbproj.schemaview
194 | *.pfx
195 | *.publishsettings
196 | node_modules/
197 | orleans.codegen.cs
198 |
199 | # Since there are multiple workflows, uncomment next line to ignore bower_components
200 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
201 | #bower_components/
202 |
203 | # RIA/Silverlight projects
204 | Generated_Code/
205 |
206 | # Backup & report files from converting an old project file
207 | # to a newer Visual Studio version. Backup files are not needed,
208 | # because we have git ;-)
209 | _UpgradeReport_Files/
210 | Backup*/
211 | UpgradeLog*.XML
212 | UpgradeLog*.htm
213 |
214 | # SQL Server files
215 | *.mdf
216 | *.ldf
217 |
218 | # Business Intelligence projects
219 | *.rdl.data
220 | *.bim.layout
221 | *.bim_*.settings
222 |
223 | # Microsoft Fakes
224 | FakesAssemblies/
225 |
226 | # GhostDoc plugin setting file
227 | *.GhostDoc.xml
228 |
229 | # Node.js Tools for Visual Studio
230 | .ntvs_analysis.dat
231 |
232 | # Visual Studio 6 build log
233 | *.plg
234 |
235 | # Visual Studio 6 workspace options file
236 | *.opt
237 |
238 | # Visual Studio LightSwitch build output
239 | **/*.HTMLClient/GeneratedArtifacts
240 | **/*.DesktopClient/GeneratedArtifacts
241 | **/*.DesktopClient/ModelManifest.xml
242 | **/*.Server/GeneratedArtifacts
243 | **/*.Server/ModelManifest.xml
244 | _Pvt_Extensions
245 |
246 | # Paket dependency manager
247 | .paket/paket.exe
248 | paket-files/
249 |
250 | # FAKE - F# Make
251 | .fake/
252 |
253 | # JetBrains Rider
254 | .idea/
255 | *.sln.iml
256 |
257 | # CodeRush
258 | .cr/
259 |
260 | # Python Tools for Visual Studio (PTVS)
261 | __pycache__/
262 | *.pyc
263 |
264 | # VS Code configurations
265 | .vscode/
266 |
267 | tools/
268 |
269 | punchclock.binlog
270 |
271 | src/*.Tests/**/ApiApprovalTests*.received.txt
272 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Code of Conduct
2 |
3 | As contributors and maintainers of this project, and in the interest of
4 | fostering an open and welcoming community, we pledge to respect all people who
5 | contribute through reporting issues, posting feature requests, updating
6 | documentation, submitting pull requests or patches, and other activities.
7 |
8 | We are committed to making participation in this project a harassment-free
9 | experience for everyone, regardless of level of experience, gender, gender
10 | identity and expression, sexual orientation, disability, personal appearance,
11 | body size, race, ethnicity, age, religion, or nationality.
12 |
13 | Examples of unacceptable behavior by participants include:
14 |
15 | * The use of sexualized language or imagery
16 | * Personal attacks
17 | * Trolling or insulting/derogatory comments
18 | * Public or private harassment
19 | * Publishing other's private information, such as physical or electronic
20 | addresses, without explicit permission
21 | * Other unethical or unprofessional conduct
22 |
23 | Project maintainers have the right and responsibility to remove, edit, or
24 | reject comments, commits, code, wiki edits, issues, and other contributions
25 | that are not aligned to this Code of Conduct, or to ban temporarily or
26 | permanently any contributor for other behaviors that they deem inappropriate,
27 | threatening, offensive, or harmful.
28 |
29 | By adopting this Code of Conduct, project maintainers commit themselves to
30 | fairly and consistently applying these principles to every aspect of managing
31 | this project. Project maintainers who do not follow or enforce the Code of
32 | Conduct may be permanently removed from the project team.
33 |
34 | This Code of Conduct applies both within project spaces and in public spaces
35 | when an individual is representing the project or its community.
36 |
37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
38 | reported by contacting a project maintainer at anais@anaisbetts.org. All
39 | complaints will be reviewed and investigated and will result in a response that
40 | is deemed necessary and appropriate to the circumstances. Maintainers are
41 | obligated to maintain confidentiality with regard to the reporter of an
42 | incident.
43 |
44 |
45 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
46 | version 1.3.0, available at
47 | [https://contributor-covenant.org/version/1/3/0/][version]
48 |
49 | [homepage]: https://contributor-covenant.org
50 | [version]: https://contributor-covenant.org/version/1/3/0/
51 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2013 Anaïs Betts
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of
4 | this software and associated documentation files (the "Software"), to deal in
5 | the Software without restriction, including without limitation the rights to
6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7 | the Software, and to permit persons to whom the Software is furnished to do so,
8 | subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://www.nuget.org/packages/punchclock) 
2 | [](https://codecov.io/gh/reactiveui/punchclock) [](https://reactiveui.net/contribute)
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | ## Punchclock: A library for managing concurrent operations
11 |
12 | Punchclock is the low-level scheduling and prioritization library used by
13 | [Fusillade](https://github.com/reactiveui/Fusillade) to orchestrate pending
14 | concurrent operations.
15 |
16 | ### What even does that mean?
17 |
18 | Ok, so you've got a shiny mobile phone app and you've got async/await.
19 | Awesome! It's so easy to issue network requests, why not do it all the time?
20 | After your users one-:star2: you for your app being slow, you discover that
21 | you're issuing *way* too many requests at the same time.
22 |
23 | Then, you try to manage issuing less requests by hand, and it becomes a
24 | spaghetti mess as different parts of your app reach into each other to try to
25 | figure out who's doing what. Let's figure out a better way.
26 |
27 | ### So many words, gimme the examples
28 |
29 | ```cs
30 | var wc = new WebClient();
31 | var opQueue = new OperationQueue(2 /*at a time*/);
32 |
33 | // Download a bunch of images
34 | var foo = opQueue.Enqueue(1,
35 | () => wc.DownloadFile("https://example.com/foo.jpg", "foo.jpg"));
36 | var bar = opQueue.Enqueue(1,
37 | () => wc.DownloadFile("https://example.com/bar.jpg", "bar.jpg"));
38 | var baz = opQueue.Enqueue(1,
39 | () => wc.DownloadFile("https://example.com/baz.jpg", "baz.jpg"));
40 | var bamf = opQueue.Enqueue(1,
41 | () => wc.DownloadFile("https://example.com/bamf.jpg", "bamf.jpg"));
42 |
43 | // We'll be downloading the images two at a time, even though we started
44 | // them all at once
45 | await Task.WaitAll(foo, bar, baz, bamf);
46 | ```
47 |
48 | Now, in a completely different part of your app, if you need something right
49 | away, you can specify it via the priority:
50 |
51 | ```cs
52 | // This file is super important, we don't care if it cuts in line in front
53 | // of some images or other stuff
54 | var wc = new WebClient();
55 | await opQueue.Enqueue(10 /* It's Important */,
56 | () => wc.DownloadFileTaskAsync("http://example.com/cool.txt", "./cool.txt"));
57 | ```
58 |
59 | ## What else can this library do
60 |
61 | * Cancellation via CancellationTokens or via Observables
62 | * Ensure certain operations don't run concurrently via a key
63 | * Queue pause / resume
64 |
65 | ## Contribute
66 |
67 | 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.
68 |
69 | So here's to you, lovely person who wants to join us — this is how you can support us:
70 |
71 | * [Responding to questions on StackOverflow](https://stackoverflow.com/questions/tagged/punchclock)
72 | * [Passing on knowledge and teaching the next generation of developers](https://ericsink.com/entries/dont_use_rxui.html)
73 | * Submitting documentation updates where you see fit or lacking.
74 | * Making contributions to the code base.
75 |
--------------------------------------------------------------------------------
/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reactiveui/punchclock/255dcc3b7b2d163250575ed306c815c1f68dcedf/images/logo.png
--------------------------------------------------------------------------------
/src/Directory.build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 | Copyright (c) .NET Foundation and Contributors
4 | MIT
5 | https://github.com/reactiveui/punchclock/
6 | .NET Foundation and Contributors
7 | xanaisbettsx;ghuntley
8 | $(NoWarn);VSX1000;SA1010
9 | AnyCPU
10 | README.md
11 | logo.png
12 | Make sure your asynchronous operations show up to work on time
13 | https://github.com/reactiveui/punchclock/releases
14 | https://github.com/reactiveui/punchclock
15 | git
16 | true
17 | $(MSBuildThisFileDirectory)analyzers.ruleset
18 | $(MSBuildProjectName.Contains('Tests'))
19 | Embedded
20 |
21 | true
22 |
23 | true
24 |
25 | $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb
26 |
27 | True
28 | latest
29 | preview
30 |
31 |
32 |
33 | false
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 |
--------------------------------------------------------------------------------
/src/Directory.build.targets:
--------------------------------------------------------------------------------
1 |
2 |
3 | $(AssemblyName) ($(TargetFramework))
4 |
5 |
6 |
7 | $(DefineConstants);NET_45;XAML
8 |
9 |
10 | $(DefineConstants);NETFX_CORE;XAML;WINDOWS_UWP
11 |
12 |
13 | $(DefineConstants);MONO;UIKIT;COCOA
14 |
15 |
16 | $(DefineConstants);MONO;COCOA
17 |
18 |
19 | $(DefineConstants);MONO;UIKIT;COCOA
20 |
21 |
22 | $(DefineConstants);MONO;UIKIT;COCOA
23 |
24 |
25 | $(DefineConstants);MONO;ANDROID
26 |
27 |
28 | $(DefineConstants);TIZEN
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/Punchclock.Tests/API/ApiApprovalTests.PunchclockTests.DotNet6_0.verified.txt:
--------------------------------------------------------------------------------
1 | [assembly: System.Runtime.Versioning.TargetFramework(".NETStandard,Version=v2.0", FrameworkDisplayName=".NET Standard 2.0")]
2 | namespace Punchclock
3 | {
4 | public class OperationQueue : System.IDisposable
5 | {
6 | public OperationQueue(int maximumConcurrent = 4) { }
7 | public void Dispose() { }
8 | protected virtual void Dispose(bool isDisposing) { }
9 | public System.IObservable EnqueueObservableOperation(int priority, System.Func> asyncCalculationFunc) { }
10 | public System.IObservable EnqueueObservableOperation(int priority, string key, System.Func> asyncCalculationFunc) { }
11 | public System.IObservable EnqueueObservableOperation(int priority, string key, System.IObservable cancel, System.Func> asyncCalculationFunc) { }
12 | public System.IDisposable PauseQueue() { }
13 | public void SetMaximumConcurrent(int maximumConcurrent) { }
14 | public System.IObservable ShutdownQueue() { }
15 | }
16 | public static class OperationQueueExtensions
17 | {
18 | public static System.Threading.Tasks.Task Enqueue(this Punchclock.OperationQueue operationQueue, int priority, System.Func asyncOperation) { }
19 | public static System.Threading.Tasks.Task Enqueue(this Punchclock.OperationQueue operationQueue, int priority, string key, System.Func asyncOperation) { }
20 | public static System.Threading.Tasks.Task Enqueue(this Punchclock.OperationQueue operationQueue, int priority, string key, System.Func asyncOperation, System.Threading.CancellationToken token) { }
21 | public static System.Threading.Tasks.Task Enqueue(this Punchclock.OperationQueue operationQueue, int priority, System.Func> asyncOperation) { }
22 | public static System.Threading.Tasks.Task Enqueue(this Punchclock.OperationQueue operationQueue, int priority, string key, System.Func> asyncOperation) { }
23 | public static System.Threading.Tasks.Task Enqueue(this Punchclock.OperationQueue operationQueue, int priority, string key, System.Func> asyncOperation, System.Threading.CancellationToken token) { }
24 | }
25 | }
--------------------------------------------------------------------------------
/src/Punchclock.Tests/API/ApiApprovalTests.PunchclockTests.DotNet7_0.verified.txt:
--------------------------------------------------------------------------------
1 | [assembly: System.Runtime.Versioning.TargetFramework(".NETStandard,Version=v2.0", FrameworkDisplayName=".NET Standard 2.0")]
2 | namespace Punchclock
3 | {
4 | public class OperationQueue : System.IDisposable
5 | {
6 | public OperationQueue(int maximumConcurrent = 4) { }
7 | public void Dispose() { }
8 | protected virtual void Dispose(bool isDisposing) { }
9 | public System.IObservable EnqueueObservableOperation(int priority, System.Func> asyncCalculationFunc) { }
10 | public System.IObservable EnqueueObservableOperation(int priority, string key, System.Func> asyncCalculationFunc) { }
11 | public System.IObservable EnqueueObservableOperation(int priority, string key, System.IObservable cancel, System.Func> asyncCalculationFunc) { }
12 | public System.IDisposable PauseQueue() { }
13 | public void SetMaximumConcurrent(int maximumConcurrent) { }
14 | public System.IObservable ShutdownQueue() { }
15 | }
16 | public static class OperationQueueExtensions
17 | {
18 | public static System.Threading.Tasks.Task Enqueue(this Punchclock.OperationQueue operationQueue, int priority, System.Func asyncOperation) { }
19 | public static System.Threading.Tasks.Task Enqueue(this Punchclock.OperationQueue operationQueue, int priority, string key, System.Func asyncOperation) { }
20 | public static System.Threading.Tasks.Task Enqueue(this Punchclock.OperationQueue operationQueue, int priority, string key, System.Func asyncOperation, System.Threading.CancellationToken token) { }
21 | public static System.Threading.Tasks.Task Enqueue(this Punchclock.OperationQueue operationQueue, int priority, System.Func> asyncOperation) { }
22 | public static System.Threading.Tasks.Task Enqueue(this Punchclock.OperationQueue operationQueue, int priority, string key, System.Func> asyncOperation) { }
23 | public static System.Threading.Tasks.Task Enqueue(this Punchclock.OperationQueue operationQueue, int priority, string key, System.Func> asyncOperation, System.Threading.CancellationToken token) { }
24 | }
25 | }
--------------------------------------------------------------------------------
/src/Punchclock.Tests/API/ApiApprovalTests.PunchclockTests.DotNet8_0.verified.txt:
--------------------------------------------------------------------------------
1 | [assembly: System.Runtime.Versioning.TargetFramework(".NETStandard,Version=v2.0", FrameworkDisplayName=".NET Standard 2.0")]
2 | namespace Punchclock
3 | {
4 | public class OperationQueue : System.IDisposable
5 | {
6 | public OperationQueue(int maximumConcurrent = 4) { }
7 | public void Dispose() { }
8 | protected virtual void Dispose(bool isDisposing) { }
9 | public System.IObservable EnqueueObservableOperation(int priority, System.Func> asyncCalculationFunc) { }
10 | public System.IObservable EnqueueObservableOperation(int priority, string key, System.Func> asyncCalculationFunc) { }
11 | public System.IObservable EnqueueObservableOperation(int priority, string key, System.IObservable cancel, System.Func> asyncCalculationFunc) { }
12 | public System.IDisposable PauseQueue() { }
13 | public void SetMaximumConcurrent(int maximumConcurrent) { }
14 | public System.IObservable ShutdownQueue() { }
15 | }
16 | public static class OperationQueueExtensions
17 | {
18 | public static System.Threading.Tasks.Task Enqueue(this Punchclock.OperationQueue operationQueue, int priority, System.Func asyncOperation) { }
19 | public static System.Threading.Tasks.Task Enqueue(this Punchclock.OperationQueue operationQueue, int priority, string key, System.Func asyncOperation) { }
20 | public static System.Threading.Tasks.Task Enqueue(this Punchclock.OperationQueue operationQueue, int priority, string key, System.Func asyncOperation, System.Threading.CancellationToken token) { }
21 | public static System.Threading.Tasks.Task Enqueue(this Punchclock.OperationQueue operationQueue, int priority, System.Func> asyncOperation) { }
22 | public static System.Threading.Tasks.Task Enqueue(this Punchclock.OperationQueue operationQueue, int priority, string key, System.Func> asyncOperation) { }
23 | public static System.Threading.Tasks.Task Enqueue(this Punchclock.OperationQueue operationQueue, int priority, string key, System.Func> asyncOperation, System.Threading.CancellationToken token) { }
24 | }
25 | }
--------------------------------------------------------------------------------
/src/Punchclock.Tests/API/ApiApprovalTests.PunchclockTests.Net4_7.verified.txt:
--------------------------------------------------------------------------------
1 | [assembly: System.Runtime.Versioning.TargetFramework(".NETStandard,Version=v2.0", FrameworkDisplayName=".NET Standard 2.0")]
2 | namespace Punchclock
3 | {
4 | public class OperationQueue : System.IDisposable
5 | {
6 | public OperationQueue(int maximumConcurrent = 4) { }
7 | public void Dispose() { }
8 | protected virtual void Dispose(bool isDisposing) { }
9 | public System.IObservable EnqueueObservableOperation(int priority, System.Func> asyncCalculationFunc) { }
10 | public System.IObservable EnqueueObservableOperation(int priority, string key, System.Func> asyncCalculationFunc) { }
11 | public System.IObservable EnqueueObservableOperation(int priority, string key, System.IObservable cancel, System.Func> asyncCalculationFunc) { }
12 | public System.IDisposable PauseQueue() { }
13 | public void SetMaximumConcurrent(int maximumConcurrent) { }
14 | public System.IObservable ShutdownQueue() { }
15 | }
16 | public static class OperationQueueExtensions
17 | {
18 | public static System.Threading.Tasks.Task Enqueue(this Punchclock.OperationQueue operationQueue, int priority, System.Func asyncOperation) { }
19 | public static System.Threading.Tasks.Task Enqueue(this Punchclock.OperationQueue operationQueue, int priority, string key, System.Func asyncOperation) { }
20 | public static System.Threading.Tasks.Task Enqueue(this Punchclock.OperationQueue operationQueue, int priority, string key, System.Func asyncOperation, System.Threading.CancellationToken token) { }
21 | public static System.Threading.Tasks.Task Enqueue(this Punchclock.OperationQueue operationQueue, int priority, System.Func> asyncOperation) { }
22 | public static System.Threading.Tasks.Task Enqueue(this Punchclock.OperationQueue operationQueue, int priority, string key, System.Func> asyncOperation) { }
23 | public static System.Threading.Tasks.Task Enqueue(this Punchclock.OperationQueue operationQueue, int priority, string key, System.Func> asyncOperation, System.Threading.CancellationToken token) { }
24 | }
25 | }
--------------------------------------------------------------------------------
/src/Punchclock.Tests/API/ApiApprovalTests.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved.
2 | // Licensed to the .NET Foundation under one or more agreements.
3 | // The .NET Foundation licenses this file to you under the MIT license.
4 | // See the LICENSE file in the project root for full license information.
5 |
6 | using System.Diagnostics.CodeAnalysis;
7 | using System.Threading.Tasks;
8 | using Xunit;
9 |
10 | namespace Punchclock.APITests;
11 |
12 | ///
13 | /// Tests for handling API approval.
14 | ///
15 | [ExcludeFromCodeCoverage]
16 | public class ApiApprovalTests
17 | {
18 | ///
19 | /// Tests to make sure the punchclock project is approved.
20 | ///
21 | /// A representing the asynchronous unit test.
22 | [Fact]
23 | public Task PunchclockTests() => typeof(OperationQueue).Assembly.CheckApproval(["Punchclock"]);
24 | }
25 |
--------------------------------------------------------------------------------
/src/Punchclock.Tests/API/ApiExtensions.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2023 .NET Foundation and Contributors. All rights reserved.
2 | // Licensed to the .NET Foundation under one or more agreements.
3 | // The .NET Foundation licenses this file to you under the MIT license.
4 | // See the LICENSE file in the project root for full license information.
5 |
6 | using System;
7 | using System.Reflection;
8 | using System.Runtime.CompilerServices;
9 | using System.Threading.Tasks;
10 | using PublicApiGenerator;
11 | using VerifyXunit;
12 |
13 | namespace Punchclock.APITests;
14 |
15 | ///
16 | /// A helper for doing API approvals.
17 | ///
18 | public static class ApiExtensions
19 | {
20 | ///
21 | /// Checks to make sure the API is approved.
22 | ///
23 | /// The assembly that is being checked.
24 | /// The namespaces.
25 | /// The caller file path.
26 | ///
27 | /// A Task.
28 | ///
29 | public static async Task CheckApproval(this Assembly assembly, string[] namespaces, [CallerFilePath] string filePath = "")
30 | {
31 | var generatorOptions = new ApiGeneratorOptions { AllowNamespacePrefixes = namespaces };
32 | var apiText = assembly.GeneratePublicApi(generatorOptions);
33 | var result = await Verifier.Verify(apiText, null, filePath)
34 | .UniqueForRuntimeAndVersion()
35 | .ScrubEmptyLines()
36 | .ScrubLines(l =>
37 | l.StartsWith("[assembly: AssemblyVersion(", StringComparison.InvariantCulture) ||
38 | l.StartsWith("[assembly: AssemblyFileVersion(", StringComparison.InvariantCulture) ||
39 | l.StartsWith("[assembly: AssemblyInformationalVersion(", StringComparison.InvariantCulture) ||
40 | l.StartsWith("[assembly: System.Reflection.AssemblyMetadata(", StringComparison.InvariantCulture));
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Punchclock.Tests/OperationQueueTests.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved.
2 | // Licensed to the .NET Foundation under one or more agreements.
3 | // The .NET Foundation licenses this file to you under the MIT license.
4 | // See the LICENSE file in the project root for full license information.
5 |
6 | using System;
7 | using System.Linq;
8 | using System.Reactive;
9 | using System.Reactive.Concurrency;
10 | using System.Reactive.Linq;
11 | using System.Reactive.Subjects;
12 | using DynamicData;
13 | using DynamicData.Binding;
14 | using Xunit;
15 |
16 | namespace Punchclock.Tests
17 | {
18 | ///
19 | /// Tests for the operation queue.
20 | ///
21 | public class OperationQueueTests
22 | {
23 | ///
24 | /// Checks to make sure that items are dispatched based on their priority.
25 | ///
26 | [Fact]
27 | public void ItemsShouldBeDispatchedByPriority()
28 | {
29 | var subjects = Enumerable.Range(0, 5).Select(x => new AsyncSubject()).ToArray();
30 | var priorities = new[] { 5, 5, 5, 10, 1, };
31 | var fixture = new OperationQueue(2);
32 |
33 | // The two at the front are solely to stop up the queue, they get subscribed
34 | // to immediately.
35 | var outputs = subjects.Zip(
36 | priorities,
37 | (inp, pri) =>
38 | {
39 | fixture
40 | .EnqueueObservableOperation(pri, () => inp)
41 | .ToObservableChangeSet(scheduler: ImmediateScheduler.Instance)
42 | .Bind(out var y).Subscribe();
43 | return y;
44 | }).ToArray();
45 |
46 | // Alright, we've got the first two subjects taking up our two live
47 | // slots, and 3,4,5 queued up. However, the order of completion should
48 | // be "4,3,5" because of the priority.
49 | Assert.True(outputs.All(x => x.Count == 0));
50 |
51 | subjects[0].OnNext(42);
52 | subjects[0].OnCompleted();
53 | Assert.Equal(new[] { 1, 0, 0, 0, 0, }, outputs.Select(x => x.Count));
54 |
55 | // 0 => completed, 1,3 => live, 2,4 => queued. Make sure 4 *doesn't* fire because
56 | // the priority should invert it.
57 | subjects[4].OnNext(42);
58 | subjects[4].OnCompleted();
59 | Assert.Equal(new[] { 1, 0, 0, 0, 0, }, outputs.Select(x => x.Count));
60 |
61 | // At the end, 0,1 => completed, 3,2 => live, 4 is queued
62 | subjects[1].OnNext(42);
63 | subjects[1].OnCompleted();
64 | Assert.Equal(new[] { 1, 1, 0, 0, 0, }, outputs.Select(x => x.Count));
65 |
66 | // At the end, 0,1,2,4 => completed, 3 is live (remember, we completed
67 | // 4 early)
68 | subjects[2].OnNext(42);
69 | subjects[2].OnCompleted();
70 | Assert.Equal(new[] { 1, 1, 1, 0, 1, }, outputs.Select(x => x.Count));
71 |
72 | subjects[3].OnNext(42);
73 | subjects[3].OnCompleted();
74 | Assert.Equal(new[] { 1, 1, 1, 1, 1, }, outputs.Select(x => x.Count));
75 | }
76 |
77 | ///
78 | /// Checks to make sure that keyed items are serialized.
79 | ///
80 | [Fact]
81 | public void KeyedItemsShouldBeSerialized()
82 | {
83 | var subj1 = new AsyncSubject();
84 | var subj2 = new AsyncSubject();
85 |
86 | var subscribeCount1 = 0;
87 | var input1Subj = new AsyncSubject();
88 | var input1 = Observable.Defer(() =>
89 | {
90 | subscribeCount1++;
91 | return input1Subj;
92 | });
93 | var subscribeCount2 = 0;
94 | var input2Subj = new AsyncSubject();
95 | var input2 = Observable.Defer(() =>
96 | {
97 | subscribeCount2++;
98 | return input2Subj;
99 | });
100 |
101 | var fixture = new OperationQueue(2);
102 |
103 | // Block up the queue
104 | foreach (var v in new[] { subj1, subj2, })
105 | {
106 | fixture.EnqueueObservableOperation(5, () => v);
107 | }
108 |
109 | // subj1,2 are live, input1,2 are in queue
110 | fixture
111 | .EnqueueObservableOperation(5, "key", Observable.Never(), () => input1)
112 | .ToObservableChangeSet(scheduler: ImmediateScheduler.Instance)
113 | .Bind(out var out1).Subscribe();
114 | fixture
115 | .EnqueueObservableOperation(5, "key", Observable.Never(), () => input2)
116 | .ToObservableChangeSet(scheduler: ImmediateScheduler.Instance)
117 | .Bind(out var out2).Subscribe();
118 |
119 | Assert.Equal(0, subscribeCount1);
120 | Assert.Equal(0, subscribeCount2);
121 |
122 | // Dispatch both subj1 and subj2, we should end up with input1 live,
123 | // but input2 in queue because of the key
124 | subj1.OnNext(42);
125 | subj1.OnCompleted();
126 | subj2.OnNext(42);
127 | subj2.OnCompleted();
128 | Assert.Equal(1, subscribeCount1);
129 | Assert.Equal(0, subscribeCount2);
130 | Assert.Empty(out1);
131 | Assert.Empty(out2);
132 |
133 | // Dispatch input1, input2 can now execute
134 | input1Subj.OnNext(42);
135 | input1Subj.OnCompleted();
136 | Assert.Equal(1, subscribeCount1);
137 | Assert.Equal(1, subscribeCount2);
138 | Assert.Single(out1);
139 | Assert.Empty(out2);
140 |
141 | // Dispatch input2, everything is finished
142 | input2Subj.OnNext(42);
143 | input2Subj.OnCompleted();
144 | Assert.Equal(1, subscribeCount1);
145 | Assert.Equal(1, subscribeCount2);
146 | Assert.Single(out1);
147 | Assert.Single(out2);
148 | }
149 |
150 | ///
151 | /// Checks to make sure that non key items are run in parallel.
152 | ///
153 | [Fact]
154 | public void NonkeyedItemsShouldRunInParallel()
155 | {
156 | var unkeyed1Subj = new AsyncSubject();
157 | var unkeyed1SubCount = 0;
158 | var unkeyed1 = Observable.Defer(() =>
159 | {
160 | unkeyed1SubCount++;
161 | return unkeyed1Subj;
162 | });
163 |
164 | var unkeyed2Subj = new AsyncSubject();
165 | var unkeyed2SubCount = 0;
166 | var unkeyed2 = Observable.Defer(() =>
167 | {
168 | unkeyed2SubCount++;
169 | return unkeyed2Subj;
170 | });
171 |
172 | var fixture = new OperationQueue(2);
173 | Assert.Equal(0, unkeyed1SubCount);
174 | Assert.Equal(0, unkeyed2SubCount);
175 |
176 | fixture.EnqueueObservableOperation(5, () => unkeyed1);
177 | fixture.EnqueueObservableOperation(5, () => unkeyed2);
178 | Assert.Equal(1, unkeyed1SubCount);
179 | Assert.Equal(1, unkeyed2SubCount);
180 | }
181 |
182 | ///
183 | /// Checks to make sure that shutdown signals once everything completes.
184 | ///
185 | [Fact]
186 | public void ShutdownShouldSignalOnceEverythingCompletes()
187 | {
188 | var subjects = Enumerable.Range(0, 5).Select(x => new AsyncSubject()).ToArray();
189 | var priorities = new[] { 5, 5, 5, 10, 1, };
190 | var fixture = new OperationQueue(2);
191 |
192 | // The two at the front are solely to stop up the queue, they get subscribed
193 | // to immediately.
194 | var outputs = subjects.Zip(
195 | priorities,
196 | (inp, pri) =>
197 | {
198 | fixture
199 | .EnqueueObservableOperation(pri, () => inp)
200 | .ToObservableChangeSet(scheduler: ImmediateScheduler.Instance)
201 | .Bind(out var output).Subscribe();
202 | return output;
203 | }).ToArray();
204 |
205 | fixture
206 | .ShutdownQueue()
207 | .ToObservableChangeSet(scheduler: ImmediateScheduler.Instance)
208 | .Bind(out var shutdown).Subscribe();
209 |
210 | Assert.True(outputs.All(x => x.Count == 0));
211 | Assert.Empty(shutdown);
212 |
213 | for (int i = 0; i < 4; i++)
214 | {
215 | subjects[i].OnNext(42);
216 | subjects[i].OnCompleted();
217 | }
218 |
219 | Assert.Empty(shutdown);
220 |
221 | // Complete the last one, that should signal that we're shut down
222 | subjects[4].OnNext(42);
223 | subjects[4].OnCompleted();
224 | Assert.True(outputs.All(x => x.Count == 1));
225 | Assert.Single(shutdown);
226 | }
227 |
228 | ///
229 | /// Checks to make sure that the queue holds items until unpaused.
230 | ///
231 | [Fact]
232 | public void PausingTheQueueShouldHoldItemsUntilUnpaused()
233 | {
234 | var item = Observable.Return(42);
235 |
236 | var fixture = new OperationQueue(2);
237 | new[]
238 | {
239 | fixture.EnqueueObservableOperation(4, () => item),
240 | fixture.EnqueueObservableOperation(4, () => item),
241 | }.Merge()
242 | .ToObservableChangeSet(scheduler: ImmediateScheduler.Instance)
243 | .Bind(out var prePauseOutput).Subscribe();
244 |
245 | Assert.Equal(2, prePauseOutput.Count);
246 |
247 | var unpause1 = fixture.PauseQueue();
248 |
249 | // The queue is halted, but we should still eventually process these
250 | // once it's no longer halted
251 | new[]
252 | {
253 | fixture.EnqueueObservableOperation(4, () => item),
254 | fixture.EnqueueObservableOperation(4, () => item),
255 | }.Merge()
256 | .ToObservableChangeSet(scheduler: ImmediateScheduler.Instance)
257 | .Bind(out var pauseOutput).Subscribe();
258 |
259 | Assert.Empty(pauseOutput);
260 |
261 | var unpause2 = fixture.PauseQueue();
262 | Assert.Empty(pauseOutput);
263 |
264 | unpause1.Dispose();
265 | Assert.Empty(pauseOutput);
266 |
267 | unpause2.Dispose();
268 | Assert.Equal(2, pauseOutput.Count);
269 | }
270 |
271 | ///
272 | /// Checks that cancelling items should not result in them being returned.
273 | ///
274 | [Fact]
275 | public void CancellingItemsShouldNotResultInThemBeingReturned()
276 | {
277 | var subj1 = new AsyncSubject();
278 | var subj2 = new AsyncSubject();
279 |
280 | var fixture = new OperationQueue(2);
281 |
282 | // Block up the queue
283 | foreach (var v in new[] { subj1, subj2, })
284 | {
285 | fixture.EnqueueObservableOperation(5, () => v);
286 | }
287 |
288 | var cancel1 = new Subject();
289 | var item1 = new AsyncSubject();
290 | new[]
291 | {
292 | fixture.EnqueueObservableOperation(5, "foo", cancel1, () => item1),
293 | fixture.EnqueueObservableOperation(5, "baz", () => Observable.Return(42)),
294 | }.Merge()
295 | .ToObservableChangeSet(scheduler: ImmediateScheduler.Instance)
296 | .Bind(out var output).Subscribe();
297 |
298 | // Still blocked by subj1,2
299 | Assert.Empty(output);
300 |
301 | // Still blocked by subj1,2, only baz is in queue
302 | cancel1.OnNext(Unit.Default);
303 | cancel1.OnCompleted();
304 | Assert.Empty(output);
305 |
306 | // foo was cancelled, baz is still good
307 | subj1.OnNext(42);
308 | subj1.OnCompleted();
309 | Assert.Single(output);
310 |
311 | // don't care that cancelled item finished
312 | item1.OnNext(42);
313 | item1.OnCompleted();
314 | Assert.Single(output);
315 |
316 | // still shouldn't see anything
317 | subj2.OnNext(42);
318 | subj2.OnCompleted();
319 | Assert.Single(output);
320 | }
321 |
322 | ///
323 | /// Checks that the cancelling of items, that the items won't be evaluated.
324 | ///
325 | [Fact]
326 | public void CancellingItemsShouldntEvenBeEvaluated()
327 | {
328 | var subj1 = new AsyncSubject();
329 | var subj2 = new AsyncSubject();
330 |
331 | var fixture = new OperationQueue(2);
332 |
333 | // Block up the queue
334 | foreach (var v in new[] { subj1, subj2, })
335 | {
336 | fixture.EnqueueObservableOperation(5, () => v);
337 | }
338 |
339 | var cancel1 = new Subject();
340 | bool wasCalled = false;
341 | var item1 = new AsyncSubject();
342 |
343 | fixture.EnqueueObservableOperation(5, "foo", cancel1, () =>
344 | {
345 | wasCalled = true;
346 | return item1;
347 | }).ToObservableChangeSet(scheduler: ImmediateScheduler.Instance)
348 | .Bind(out var output).Subscribe();
349 |
350 | // Still blocked by subj1,2
351 | Assert.Empty(output);
352 | Assert.False(wasCalled);
353 |
354 | // Still blocked by subj1,2 - however, we've cancelled foo before
355 | // it even had a chance to run - if that's the case, we shouldn't
356 | // even call the evaluation func
357 | cancel1.OnNext(Unit.Default);
358 | cancel1.OnCompleted();
359 | Assert.Empty(output);
360 | Assert.False(wasCalled);
361 |
362 | // Unblock subj1,2, we still shouldn't see wasCalled = true
363 | subj1.OnNext(42);
364 | subj1.OnCompleted();
365 | Assert.Empty(output);
366 | Assert.False(wasCalled);
367 |
368 | subj2.OnNext(42);
369 | subj2.OnCompleted();
370 | Assert.Empty(output);
371 | Assert.False(wasCalled);
372 | }
373 |
374 | ///
375 | /// Checks to make sure the queue respects maximum concurrency.
376 | ///
377 | [Fact]
378 | public void QueueShouldRespectMaximumConcurrent()
379 | {
380 | var unkeyed1Subj = new AsyncSubject();
381 | var unkeyed1SubCount = 0;
382 | var unkeyed1 = Observable.Defer(() =>
383 | {
384 | unkeyed1SubCount++;
385 | return unkeyed1Subj;
386 | });
387 |
388 | var unkeyed2Subj = new AsyncSubject();
389 | var unkeyed2SubCount = 0;
390 | var unkeyed2 = Observable.Defer(() =>
391 | {
392 | unkeyed2SubCount++;
393 | return unkeyed2Subj;
394 | });
395 |
396 | var unkeyed3Subj = new AsyncSubject();
397 | var unkeyed3SubCount = 0;
398 | var unkeyed3 = Observable.Defer(() =>
399 | {
400 | unkeyed3SubCount++;
401 | return unkeyed3Subj;
402 | });
403 |
404 | var fixture = new OperationQueue(2);
405 | Assert.Equal(0, unkeyed1SubCount);
406 | Assert.Equal(0, unkeyed2SubCount);
407 | Assert.Equal(0, unkeyed3SubCount);
408 |
409 | fixture.EnqueueObservableOperation(5, () => unkeyed1);
410 | fixture.EnqueueObservableOperation(5, () => unkeyed2);
411 | fixture.EnqueueObservableOperation(5, () => unkeyed3);
412 |
413 | Assert.Equal(1, unkeyed1SubCount);
414 | Assert.Equal(1, unkeyed2SubCount);
415 | Assert.Equal(0, unkeyed3SubCount);
416 | }
417 |
418 | ///
419 | /// Checks to see if the maximum concurrency is increased that the existing queue adapts.
420 | ///
421 | [Fact]
422 | public void ShouldBeAbleToIncreaseTheMaximunConcurrentValueOfAnExistingQueue()
423 | {
424 | var unkeyed1Subj = new AsyncSubject();
425 | var unkeyed1SubCount = 0;
426 | var unkeyed1 = Observable.Defer(() =>
427 | {
428 | unkeyed1SubCount++;
429 | return unkeyed1Subj;
430 | });
431 |
432 | var unkeyed2Subj = new AsyncSubject();
433 | var unkeyed2SubCount = 0;
434 | var unkeyed2 = Observable.Defer(() =>
435 | {
436 | unkeyed2SubCount++;
437 | return unkeyed2Subj;
438 | });
439 |
440 | var unkeyed3Subj = new AsyncSubject();
441 | var unkeyed3SubCount = 0;
442 | var unkeyed3 = Observable.Defer(() =>
443 | {
444 | unkeyed3SubCount++;
445 | return unkeyed3Subj;
446 | });
447 |
448 | var unkeyed4Subj = new AsyncSubject();
449 | var unkeyed4SubCount = 0;
450 | var unkeyed4 = Observable.Defer(() =>
451 | {
452 | unkeyed4SubCount++;
453 | return unkeyed4Subj;
454 | });
455 |
456 | var fixture = new OperationQueue(2);
457 | Assert.Equal(0, unkeyed1SubCount);
458 | Assert.Equal(0, unkeyed2SubCount);
459 | Assert.Equal(0, unkeyed3SubCount);
460 | Assert.Equal(0, unkeyed4SubCount);
461 |
462 | fixture.EnqueueObservableOperation(5, () => unkeyed1);
463 | fixture.EnqueueObservableOperation(5, () => unkeyed2);
464 | fixture.EnqueueObservableOperation(5, () => unkeyed3);
465 | fixture.EnqueueObservableOperation(5, () => unkeyed4);
466 |
467 | Assert.Equal(1, unkeyed1SubCount);
468 | Assert.Equal(1, unkeyed2SubCount);
469 | Assert.Equal(0, unkeyed3SubCount);
470 | Assert.Equal(0, unkeyed4SubCount);
471 |
472 | fixture.SetMaximumConcurrent(3);
473 |
474 | Assert.Equal(1, unkeyed1SubCount);
475 | Assert.Equal(1, unkeyed2SubCount);
476 | Assert.Equal(1, unkeyed3SubCount);
477 | Assert.Equal(0, unkeyed4SubCount);
478 | }
479 |
480 | ///
481 | /// Checks to make sure that decreasing the maximum concurrency the queue adapts.
482 | ///
483 | [Fact]
484 | public void ShouldBeAbleToDecreaseTheMaximunConcurrentValueOfAnExistingQueue()
485 | {
486 | var subjects = Enumerable.Range(0, 6).Select(x => new AsyncSubject()).ToArray();
487 | var fixture = new OperationQueue(3);
488 |
489 | // The three at the front are solely to stop up the queue, they get subscribed
490 | // to immediately.
491 | var outputs = subjects
492 | .Select(inp =>
493 | {
494 | fixture
495 | .EnqueueObservableOperation(5, () => inp)
496 | .ToObservableChangeSet(scheduler: ImmediateScheduler.Instance)
497 | .Bind(out var output).Subscribe();
498 | return output;
499 | }).ToArray();
500 |
501 | Assert.True(
502 | new[] { true, true, true, false, false, false, }.Zip(
503 | subjects,
504 | (expected, subj) => new { expected, actual = subj.HasObservers, })
505 | .All(x => x.expected == x.actual));
506 |
507 | fixture.SetMaximumConcurrent(2);
508 |
509 | // Complete the first one, the last three subjects should still have
510 | // no observers because we reduced maximum concurrent
511 | subjects[0].OnNext(42);
512 | subjects[0].OnCompleted();
513 |
514 | Assert.True(
515 | new[] { false, true, true, false, false, false, }.Zip(
516 | subjects,
517 | (expected, subj) => new { expected, actual = subj.HasObservers, })
518 | .All(x => x.expected == x.actual));
519 |
520 | // Complete subj[1], now 2,3 are live
521 | subjects[1].OnNext(42);
522 | subjects[1].OnCompleted();
523 |
524 | Assert.True(
525 | new[] { false, false, true, true, false, false, }.Zip(
526 | subjects,
527 | (expected, subj) => new { expected, actual = subj.HasObservers, })
528 | .All(x => x.expected == x.actual));
529 | }
530 | }
531 | }
532 |
--------------------------------------------------------------------------------
/src/Punchclock.Tests/Punchclock.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net6.0;net8.0
4 | $(TargetFrameworks);net472
5 | $(NoWarn);1591;CA1707;SA1633
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | all
23 | runtime; build; native; contentfiles; analyzers
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/Punchclock.sln:
--------------------------------------------------------------------------------
1 | Microsoft Visual Studio Solution File, Format Version 12.00
2 | # 17
3 | VisualStudioVersion = 17.3.32922.545
4 | MinimumVisualStudioVersion = 10.0.40219.1
5 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Punchclock", "Punchclock\Punchclock.csproj", "{D3D5E08E-2DAA-4C14-BDF1-C15BD81247F5}"
6 | EndProject
7 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Punchclock.Tests", "Punchclock.Tests\Punchclock.Tests.csproj", "{1966B9AC-F962-4AAD-9B17-E581D12619A1}"
8 | EndProject
9 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Items", "Items", "{E035091F-9C2B-4F6D-924F-08EC78AB9E43}"
10 | ProjectSection(SolutionItems) = preProject
11 | analyzers.ruleset = analyzers.ruleset
12 | analyzers.tests.ruleset = analyzers.tests.ruleset
13 | Directory.build.props = Directory.build.props
14 | ..\README.md = ..\README.md
15 | stylecop.json = stylecop.json
16 | ..\version.json = ..\version.json
17 | EndProjectSection
18 | EndProject
19 | Global
20 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
21 | Debug|Any CPU = Debug|Any CPU
22 | Release|Any CPU = Release|Any CPU
23 | EndGlobalSection
24 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
25 | {D3D5E08E-2DAA-4C14-BDF1-C15BD81247F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
26 | {D3D5E08E-2DAA-4C14-BDF1-C15BD81247F5}.Debug|Any CPU.Build.0 = Debug|Any CPU
27 | {D3D5E08E-2DAA-4C14-BDF1-C15BD81247F5}.Release|Any CPU.ActiveCfg = Release|Any CPU
28 | {D3D5E08E-2DAA-4C14-BDF1-C15BD81247F5}.Release|Any CPU.Build.0 = Release|Any CPU
29 | {1966B9AC-F962-4AAD-9B17-E581D12619A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
30 | {1966B9AC-F962-4AAD-9B17-E581D12619A1}.Debug|Any CPU.Build.0 = Debug|Any CPU
31 | {1966B9AC-F962-4AAD-9B17-E581D12619A1}.Release|Any CPU.ActiveCfg = Release|Any CPU
32 | {1966B9AC-F962-4AAD-9B17-E581D12619A1}.Release|Any CPU.Build.0 = Release|Any CPU
33 | EndGlobalSection
34 | GlobalSection(SolutionProperties) = preSolution
35 | HideSolutionNode = FALSE
36 | EndGlobalSection
37 | GlobalSection(ExtensibilityGlobals) = postSolution
38 | SolutionGuid = {5CDD70E5-DB77-4301-8EB4-706CCACF4459}
39 | EndGlobalSection
40 | EndGlobal
41 |
--------------------------------------------------------------------------------
/src/Punchclock/KeyedOperation.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved.
2 | // Licensed to the .NET Foundation under one or more agreements.
3 | // The .NET Foundation licenses this file to you under the MIT license.
4 | // See the LICENSE file in the project root for full license information.
5 |
6 | using System;
7 | using System.Diagnostics.CodeAnalysis;
8 | using System.Reactive;
9 | using System.Reactive.Linq;
10 | using System.Reactive.Subjects;
11 |
12 | namespace Punchclock;
13 |
14 | internal abstract class KeyedOperation : IComparable
15 | {
16 | public bool CancelledEarly { get; set; }
17 |
18 | public int Priority { get; set; }
19 |
20 | public int Id { get; set; }
21 |
22 | public string? Key { get; set; }
23 |
24 | public IObservable? CancelSignal { get; set; }
25 |
26 | public bool KeyIsDefault => string.IsNullOrEmpty(Key) || Key == OperationQueue.DefaultKey;
27 |
28 | public abstract IObservable EvaluateFunc();
29 |
30 | public int CompareTo(KeyedOperation other)
31 | {
32 | // NB: Non-keyed operations always come before keyed operations in
33 | // order to make sure that serialized keyed operations don't take
34 | // up concurrency slots
35 | if (KeyIsDefault != other.KeyIsDefault)
36 | {
37 | return KeyIsDefault ? 1 : -1;
38 | }
39 |
40 | return other.Priority.CompareTo(Priority);
41 | }
42 | }
43 |
44 | [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Generic implementation of same class name.")]
45 | internal class KeyedOperation : KeyedOperation
46 | {
47 | public Func>? Func { get; set; }
48 |
49 | public ReplaySubject Result { get; } = new ReplaySubject();
50 |
51 | public override IObservable EvaluateFunc()
52 | {
53 | if (Func == null)
54 | {
55 | return Observable.Empty();
56 | }
57 |
58 | if (CancelledEarly)
59 | {
60 | return Observable.Empty();
61 | }
62 |
63 | var signal = CancelSignal ?? Observable.Empty();
64 | var ret = Func().TakeUntil(signal).Multicast(Result);
65 | ret.Connect();
66 |
67 | return ret.Select(_ => Unit.Default);
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/Punchclock/OperationQueue.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved.
2 | // Licensed to the .NET Foundation under one or more agreements.
3 | // The .NET Foundation licenses this file to you under the MIT license.
4 | // See the LICENSE file in the project root for full license information.
5 |
6 | using System;
7 | using System.Diagnostics;
8 | using System.Reactive;
9 | using System.Reactive.Concurrency;
10 | using System.Reactive.Disposables;
11 | using System.Reactive.Linq;
12 | using System.Reactive.Subjects;
13 | using System.Threading;
14 |
15 | namespace Punchclock;
16 |
17 | ///
18 | /// OperationQueue is the core of PunchClock, and represents a scheduler for
19 | /// deferred actions, such as network requests. This scheduler supports
20 | /// scheduling via priorities, as well as serializing requests that access
21 | /// the same data.
22 | ///
23 | /// The queue allows a fixed number of concurrent in-flight operations at a
24 | /// time. When there are available "slots", items are dispatched as they come
25 | /// in. When the slots are full, the queueing policy starts to apply.
26 | ///
27 | /// The queue, similar to Akavache's KeyedOperationQueue, also allows keys to
28 | /// be specified to serialize operations - if you have three "foo" items, they
29 | /// will wait in line and only one "foo" can run. However, a "bar" and "baz"
30 | /// item can run at the same time as a "foo" item.
31 | ///
32 | public class OperationQueue : IDisposable
33 | {
34 | private static int sequenceNumber;
35 |
36 | private readonly Subject _queuedOps = new();
37 | private readonly IConnectableObservable _resultObs;
38 | private readonly PrioritySemaphoreSubject _scheduledGate;
39 | private int _maximumConcurrent;
40 | private int _pauseRefCount;
41 | private bool _isDisposed;
42 |
43 | private AsyncSubject? _shutdownObs;
44 |
45 | ///
46 | /// Initializes a new instance of the class.
47 | ///
48 | /// The maximum number of concurrent operations.
49 | public OperationQueue(int maximumConcurrent = 4)
50 | {
51 | _maximumConcurrent = maximumConcurrent;
52 | _scheduledGate = new(maximumConcurrent);
53 |
54 | _resultObs = _queuedOps
55 | .Multicast(_scheduledGate).RefCount()
56 | .GroupBy(x => x.Key)
57 | .Select(x =>
58 | {
59 | var ret = x.Select(
60 | y => ProcessOperation(y)
61 | .TakeUntil(y.CancelSignal ?? Observable.Empty())
62 | .Finally(() => _scheduledGate.Release()));
63 | return x.Key == DefaultKey ? ret.Merge() : ret.Concat();
64 | })
65 | .Merge()
66 | .Multicast(new Subject());
67 |
68 | _resultObs.Connect();
69 | }
70 |
71 | ///
72 | /// Gets the default key used if there is no item.
73 | ///
74 | internal static string DefaultKey { get; } = "__NONE__";
75 |
76 | ///
77 | /// This method enqueues an action to be run at a later time, according
78 | /// to the scheduling policies (i.e. via priority and key).
79 | ///
80 | /// The type of item for the observable.
81 | /// Used to allow any observable type.
82 | /// The priority of operation. Higher priorities run before lower ones.
83 | /// A key to apply to the operation. Items with the same key will be run in order.
84 | /// A observable which if signalled, the operation will be cancelled.
85 | /// The async method to execute when scheduled.
86 | /// The result of the async calculation.
87 | public IObservable EnqueueObservableOperation(int priority, string key, IObservable cancel, Func> asyncCalculationFunc)
88 | {
89 | var id = Interlocked.Increment(ref sequenceNumber);
90 | var cancelReplay = new ReplaySubject();
91 |
92 | var item = new KeyedOperation
93 | {
94 | Key = key ?? DefaultKey,
95 | Id = id,
96 | Priority = priority,
97 | CancelSignal = cancelReplay.Select(_ => Unit.Default),
98 | Func = asyncCalculationFunc,
99 | };
100 |
101 | cancel
102 | .Do(_ =>
103 | {
104 | Debug.WriteLine("Cancelling {0}", id);
105 | item.CancelledEarly = true;
106 | })
107 | .Multicast(cancelReplay).Connect();
108 |
109 | lock (_queuedOps)
110 | {
111 | Debug.WriteLine("Queued item {0}, priority {1}", item.Id, item.Priority);
112 | _queuedOps.OnNext(item);
113 | }
114 |
115 | return item.Result;
116 | }
117 |
118 | ///
119 | /// This method enqueues an action to be run at a later time, according
120 | /// to the scheduling policies (i.e. via priority and key).
121 | ///
122 | /// The type of item for the observable.
123 | /// Higher priorities run before lower ones.
124 | /// Items with the same key will be run in order.
125 | /// The async method to execute when scheduled.
126 | /// The result of the async calculation.
127 | public IObservable EnqueueObservableOperation(int priority, string key, Func> asyncCalculationFunc) =>
128 | EnqueueObservableOperation(priority, key, Observable.Never(), asyncCalculationFunc);
129 |
130 | ///
131 | /// This method enqueues an action to be run at a later time, according
132 | /// to the scheduling policies (i.e. via priority).
133 | ///
134 | /// The type of item for the observable.
135 | /// Higher priorities run before lower ones.
136 | /// The async method to execute when scheduled.
137 | /// The result of the async calculation.
138 | public IObservable EnqueueObservableOperation(int priority, Func> asyncCalculationFunc) =>
139 | EnqueueObservableOperation(priority, DefaultKey, Observable.Never(), asyncCalculationFunc);
140 |
141 | ///
142 | /// This method pauses the dispatch queue. Inflight operations will not
143 | /// be canceled, but new ones will not be processed until the queue is
144 | /// resumed.
145 | ///
146 | /// A Disposable that resumes the queue when disposed.
147 | public IDisposable PauseQueue()
148 | {
149 | if (Interlocked.Increment(ref _pauseRefCount) == 1)
150 | {
151 | _scheduledGate.MaximumCount = 0;
152 | }
153 |
154 | return Disposable.Create(() =>
155 | {
156 | if (Interlocked.Decrement(ref _pauseRefCount) > 0)
157 | {
158 | return;
159 | }
160 |
161 | if (_shutdownObs != null)
162 | {
163 | return;
164 | }
165 |
166 | _scheduledGate.MaximumCount = _maximumConcurrent;
167 | });
168 | }
169 |
170 | ///
171 | /// Sets the maximum level of concurrency for the operation queue.
172 | ///
173 | /// The maximum amount of concurrency.
174 | public void SetMaximumConcurrent(int maximumConcurrent)
175 | {
176 | using (PauseQueue())
177 | {
178 | _maximumConcurrent = maximumConcurrent;
179 | }
180 | }
181 |
182 | ///
183 | /// Shuts down the queue and notifies when all outstanding items have
184 | /// been processed.
185 | ///
186 | /// An Observable that will signal when all items are complete.
187 | ///
188 | public IObservable ShutdownQueue()
189 | {
190 | lock (_queuedOps)
191 | {
192 | if (_shutdownObs != null)
193 | {
194 | return _shutdownObs;
195 | }
196 |
197 | _shutdownObs = new AsyncSubject();
198 |
199 | // Disregard paused queue
200 | _scheduledGate.MaximumCount = _maximumConcurrent;
201 |
202 | _queuedOps.OnCompleted();
203 |
204 | _resultObs.Materialize()
205 | .Where(x => x.Kind != NotificationKind.OnNext)
206 | .SelectMany(x =>
207 | x.Kind == NotificationKind.OnError ?
208 | Observable.Throw(x.Exception!) :
209 | Observable.Return(Unit.Default))
210 | .Multicast(_shutdownObs)
211 | .Connect();
212 |
213 | return _shutdownObs;
214 | }
215 | }
216 |
217 | ///
218 | public void Dispose()
219 | {
220 | Dispose(true);
221 | GC.SuppressFinalize(this);
222 | }
223 |
224 | ///
225 | /// Disposes managed resources that are disposable and handles cleanup of unmanaged items.
226 | ///
227 | /// If we are disposing managed resources.
228 | protected virtual void Dispose(bool isDisposing)
229 | {
230 | if (_isDisposed)
231 | {
232 | return;
233 | }
234 |
235 | if (isDisposing)
236 | {
237 | _queuedOps?.Dispose();
238 | _shutdownObs?.Dispose();
239 | }
240 |
241 | _isDisposed = true;
242 | }
243 |
244 | private static IObservable ProcessOperation(KeyedOperation operation)
245 | {
246 | Debug.WriteLine("Processing item {0}, priority {1}", operation.Id, operation.Priority);
247 | return Observable.Defer(operation.EvaluateFunc)
248 | .Select(_ => operation)
249 | .Catch(Observable.Return(operation));
250 | }
251 | }
252 |
--------------------------------------------------------------------------------
/src/Punchclock/OperationQueueExtensions.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved.
2 | // Licensed to the .NET Foundation under one or more agreements.
3 | // The .NET Foundation licenses this file to you under the MIT license.
4 | // See the LICENSE file in the project root for full license information.
5 |
6 | using System;
7 | using System.Reactive;
8 | using System.Reactive.Linq;
9 | using System.Reactive.Subjects;
10 | using System.Reactive.Threading.Tasks;
11 | using System.Threading;
12 | using System.Threading.Tasks;
13 |
14 | namespace Punchclock;
15 |
16 | ///
17 | /// Extension methods associated with the .
18 | ///
19 | public static class OperationQueueExtensions
20 | {
21 | ///
22 | /// Adds a operation to the operation queue.
23 | ///
24 | /// The type of item contained within our observable.
25 | /// The operation queue to add our operation to.
26 | /// The priority of operation. Higher priorities run before lower ones.
27 | /// A key to apply to the operation. Items with the same key will be run in order.
28 | /// The async method to execute when scheduled.
29 | /// A cancellation token which if signalled, the operation will be cancelled.
30 | /// A task to monitor the progress.
31 | public static Task Enqueue(this OperationQueue operationQueue, int priority, string key, Func> asyncOperation, CancellationToken token)
32 | {
33 | if (operationQueue == null)
34 | {
35 | throw new ArgumentNullException(nameof(operationQueue));
36 | }
37 |
38 | return operationQueue.EnqueueObservableOperation(priority, key, ConvertTokenToObservable(token), () => asyncOperation().ToObservable())
39 | .ToTask(token);
40 | }
41 |
42 | ///
43 | /// Adds a operation to the operation queue.
44 | ///
45 | /// The operation queue to add our operation to.
46 | /// The priority of operation. Higher priorities run before lower ones.
47 | /// A key to apply to the operation. Items with the same key will be run in order.
48 | /// The async method to execute when scheduled.
49 | /// A cancellation token which if signalled, the operation will be cancelled.
50 | /// A task to monitor the progress.
51 | public static Task Enqueue(this OperationQueue operationQueue, int priority, string key, Func asyncOperation, CancellationToken token)
52 | {
53 | if (operationQueue == null)
54 | {
55 | throw new ArgumentNullException(nameof(operationQueue));
56 | }
57 |
58 | return operationQueue.EnqueueObservableOperation(priority, key, ConvertTokenToObservable(token), () => asyncOperation().ToObservable())
59 | .ToTask(token);
60 | }
61 |
62 | ///
63 | /// Adds a operation to the operation queue.
64 | ///
65 | /// The type of item contained within our observable.
66 | /// The operation queue to add our operation to.
67 | /// The priority of operation. Higher priorities run before lower ones.
68 | /// A key to apply to the operation. Items with the same key will be run in order.
69 | /// The async method to execute when scheduled.
70 | /// A task to monitor the progress.
71 | public static Task Enqueue(this OperationQueue operationQueue, int priority, string key, Func> asyncOperation)
72 | {
73 | if (operationQueue == null)
74 | {
75 | throw new ArgumentNullException(nameof(operationQueue));
76 | }
77 |
78 | return operationQueue.EnqueueObservableOperation(priority, key, Observable.Never(), () => asyncOperation().ToObservable())
79 | .ToTask();
80 | }
81 |
82 | ///
83 | /// Adds a operation to the operation queue.
84 | ///
85 | /// The operation queue to add our operation to.
86 | /// The priority of operation. Higher priorities run before lower ones.
87 | /// A key to apply to the operation. Items with the same key will be run in order.
88 | /// The async method to execute when scheduled.
89 | /// A task to monitor the progress.
90 | public static Task Enqueue(this OperationQueue operationQueue, int priority, string key, Func asyncOperation)
91 | {
92 | if (operationQueue == null)
93 | {
94 | throw new ArgumentNullException(nameof(operationQueue));
95 | }
96 |
97 | return operationQueue.EnqueueObservableOperation(priority, key, Observable.Never(), () => asyncOperation().ToObservable())
98 | .ToTask();
99 | }
100 |
101 | ///
102 | /// Adds a operation to the operation queue.
103 | ///
104 | /// The type of item contained within our observable.
105 | /// The operation queue to add our operation to.
106 | /// The priority of operation. Higher priorities run before lower ones.
107 | /// The async method to execute when scheduled.
108 | /// A task to monitor the progress.
109 | public static Task Enqueue(this OperationQueue operationQueue, int priority, Func> asyncOperation)
110 | {
111 | if (operationQueue == null)
112 | {
113 | throw new ArgumentNullException(nameof(operationQueue));
114 | }
115 |
116 | return operationQueue.EnqueueObservableOperation(priority, () => asyncOperation().ToObservable())
117 | .ToTask();
118 | }
119 |
120 | ///
121 | /// Adds a operation to the operation queue.
122 | ///
123 | /// The operation queue to add our operation to.
124 | /// The priority of operation. Higher priorities run before lower ones.
125 | /// The async method to execute when scheduled.
126 | /// A task to monitor the progress.
127 | public static Task Enqueue(this OperationQueue operationQueue, int priority, Func asyncOperation)
128 | {
129 | if (operationQueue == null)
130 | {
131 | throw new ArgumentNullException(nameof(operationQueue));
132 | }
133 |
134 | return operationQueue.EnqueueObservableOperation(priority, () => asyncOperation().ToObservable())
135 | .ToTask();
136 | }
137 |
138 | private static IObservable ConvertTokenToObservable(CancellationToken token)
139 | {
140 | var cancel = new AsyncSubject();
141 |
142 | if (token.IsCancellationRequested)
143 | {
144 | return Observable.Throw(new ArgumentException("Token is already cancelled"));
145 | }
146 |
147 | token.Register(() =>
148 | {
149 | cancel.OnNext(Unit.Default);
150 | cancel.OnCompleted();
151 | });
152 | return cancel;
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/src/Punchclock/PriorityQueue.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved.
2 | // Licensed to the .NET Foundation under one or more agreements.
3 | // The .NET Foundation licenses this file to you under the MIT license.
4 | // See the LICENSE file in the project root for full license information.
5 |
6 | using System;
7 | using System.Collections.Generic;
8 | using System.Threading;
9 |
10 | namespace Punchclock;
11 |
12 | ///
13 | /// A priority queue which will store items contained in order of the various priorities.
14 | ///
15 | /// The type of item to store in the queue.
16 | ///
17 | /// Based off Microsoft internal code.
18 | /// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
19 | /// This is https://github.com/mono/rx/blob/master/Rx/NET/Source/System.Reactive.Core/Reactive/Internal/PriorityQueue.cs originally.
20 | ///
21 | ///
22 | /// Initializes a new instance of the class.
23 | ///
24 | /// The starting capacity of the queue.
25 | internal class PriorityQueue(int capacity)
26 | where T : IComparable
27 | {
28 | private const int DefaultCapacity = 16;
29 |
30 | #if !NO_INTERLOCKED_64
31 | private static long _count = long.MinValue;
32 | #else
33 | private static int _count = int.MinValue;
34 | #endif
35 | private IndexedItem[] _items = new IndexedItem[capacity];
36 |
37 | ///
38 | /// Initializes a new instance of the class.
39 | ///
40 | public PriorityQueue()
41 | : this(DefaultCapacity)
42 | {
43 | }
44 |
45 | ///
46 | /// Gets the number of items inside the queue.
47 | ///
48 | public int Count { get; private set; }
49 |
50 | ///
51 | /// Peeks at the next time available in the queue.
52 | ///
53 | /// The next item.
54 | public T Peek()
55 | {
56 | if (Count == 0)
57 | {
58 | throw new InvalidOperationException("There are no items in the collection");
59 | }
60 |
61 | return _items[0].Value;
62 | }
63 |
64 | ///
65 | /// Removes and returns the next item in the queue.
66 | ///
67 | /// The next item.
68 | public T Dequeue()
69 | {
70 | var result = Peek();
71 | RemoveAt(0, true);
72 | return result;
73 | }
74 |
75 | ///
76 | /// Removes up to the specified number of items and returns those items.
77 | ///
78 | /// The maximum number of items to remove from the queue.
79 | /// The next items.
80 | public T[] DequeueSome(int count)
81 | {
82 | if (count == 0)
83 | {
84 | return [];
85 | }
86 |
87 | var ret = new T[count];
88 | count = Math.Min(count, Count);
89 | for (int i = 0; i < count; i++)
90 | {
91 | ret[i] = Peek();
92 | RemoveAt(0, false);
93 | }
94 |
95 | return ret;
96 | }
97 |
98 | ///
99 | /// Removes all the items currently contained within the queue and returns them.
100 | ///
101 | /// All the items from the queue.
102 | public T[] DequeueAll() => DequeueSome(Count);
103 |
104 | ///
105 | /// Adds a item in the correct location based on priority to the queue.
106 | ///
107 | /// The item to add.
108 | public void Enqueue(T item)
109 | {
110 | if (Count >= _items.Length)
111 | {
112 | var temp = _items;
113 | _items = new IndexedItem[_items.Length * 2];
114 | Array.Copy(temp, _items, temp.Length);
115 | }
116 |
117 | var index = Count++;
118 | _items[index] = new IndexedItem { Value = item, Id = Interlocked.Increment(ref _count) };
119 | Percolate(index);
120 | }
121 |
122 | ///
123 | /// Removes the specified item from the queue.
124 | ///
125 | /// The item to remove.
126 | /// If the remove was successful or not.
127 | public bool Remove(T item)
128 | {
129 | for (var i = 0; i < Count; ++i)
130 | {
131 | if (EqualityComparer.Default.Equals(_items[i].Value, item))
132 | {
133 | RemoveAt(i, false);
134 | return true;
135 | }
136 | }
137 |
138 | return false;
139 | }
140 |
141 | private bool IsHigherPriority(int left, int right) => _items[left].CompareTo(_items[right]) < 0;
142 |
143 | private void Percolate(int index)
144 | {
145 | if (index >= Count || index < 0)
146 | {
147 | return;
148 | }
149 |
150 | var parent = (index - 1) / 2;
151 | if (parent < 0 || parent == index)
152 | {
153 | return;
154 | }
155 |
156 | if (IsHigherPriority(index, parent))
157 | {
158 | (_items[parent], _items[index]) = (_items[index], _items[parent]);
159 | Percolate(parent);
160 | }
161 | }
162 |
163 | private void Heapify() => Heapify(0);
164 |
165 | private void Heapify(int index)
166 | {
167 | if (index >= Count || index < 0)
168 | {
169 | return;
170 | }
171 |
172 | var left = (2 * index) + 1;
173 | var right = (2 * index) + 2;
174 | var first = index;
175 |
176 | if (left < Count && IsHigherPriority(left, first))
177 | {
178 | first = left;
179 | }
180 |
181 | if (right < Count && IsHigherPriority(right, first))
182 | {
183 | first = right;
184 | }
185 |
186 | if (first != index)
187 | {
188 | (_items[first], _items[index]) = (_items[index], _items[first]);
189 | Heapify(first);
190 | }
191 | }
192 |
193 | private void RemoveAt(int index, bool single)
194 | {
195 | _items[index] = _items[--Count];
196 | _items[Count] = default;
197 | Heapify();
198 | if (Count < _items.Length / 4 && (single || Count < DefaultCapacity))
199 | {
200 | var temp = _items;
201 | _items = new IndexedItem[_items.Length / 2];
202 | Array.Copy(temp, 0, _items, 0, Count);
203 | }
204 | }
205 |
206 | private struct IndexedItem : IComparable
207 | {
208 | public T Value;
209 | #if !NO_INTERLOCKED_64
210 | public long Id;
211 | #else
212 | public int Id;
213 | #endif
214 |
215 | public int CompareTo(IndexedItem other)
216 | {
217 | var c = Value.CompareTo(other.Value);
218 | if (c == 0)
219 | {
220 | c = Id.CompareTo(other.Id);
221 | }
222 |
223 | return c;
224 | }
225 | }
226 | }
227 |
--------------------------------------------------------------------------------
/src/Punchclock/PrioritySemaphoreSubject.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved.
2 | // Licensed to the .NET Foundation under one or more agreements.
3 | // The .NET Foundation licenses this file to you under the MIT license.
4 | // See the LICENSE file in the project root for full license information.
5 |
6 | using System;
7 | using System.Reactive.Concurrency;
8 | using System.Reactive.Subjects;
9 | using System.Threading;
10 |
11 | namespace Punchclock;
12 |
13 | internal class PrioritySemaphoreSubject : ISubject
14 | where T : IComparable
15 | {
16 | private readonly ISubject _inner;
17 | private PriorityQueue _nextItems = new();
18 | private int _count;
19 |
20 | private int _MaximumCount;
21 |
22 | ///
23 | /// Initializes a new instance of the class.
24 | ///
25 | /// The maximum number of items to allow.
26 | /// The scheduler to use when emitting the items.
27 | public PrioritySemaphoreSubject(int maxCount, IScheduler? sched = null)
28 | {
29 | _inner = sched != null ? new ScheduledSubject(sched) : new Subject();
30 | MaximumCount = maxCount;
31 | }
32 |
33 | ///
34 | /// Gets or sets the maximum count to allow.
35 | ///
36 | public int MaximumCount
37 | {
38 | get => _MaximumCount;
39 | set
40 | {
41 | _MaximumCount = value;
42 | YieldUntilEmptyOrBlocked();
43 | }
44 | }
45 |
46 | ///
47 | public void OnNext(T value)
48 | {
49 | var queue = Interlocked.CompareExchange(ref _nextItems, null!, null!);
50 | if (queue == null)
51 | {
52 | return;
53 | }
54 |
55 | lock (queue)
56 | {
57 | queue.Enqueue(value);
58 | }
59 |
60 | YieldUntilEmptyOrBlocked();
61 | }
62 |
63 | ///
64 | /// Releases a reference counted value.
65 | ///
66 | public void Release()
67 | {
68 | Interlocked.Decrement(ref _count);
69 | YieldUntilEmptyOrBlocked();
70 | }
71 |
72 | ///
73 | public void OnCompleted()
74 | {
75 | var queue = Interlocked.Exchange(ref _nextItems, null!);
76 | if (queue == null)
77 | {
78 | return;
79 | }
80 |
81 | T[] items;
82 | lock (queue)
83 | {
84 | items = queue.DequeueAll();
85 | }
86 |
87 | foreach (var v in items)
88 | {
89 | _inner.OnNext(v);
90 | }
91 |
92 | _inner.OnCompleted();
93 | }
94 |
95 | ///
96 | public void OnError(Exception error)
97 | {
98 | Interlocked.Exchange(ref _nextItems, null!);
99 | _inner.OnError(error);
100 | }
101 |
102 | ///
103 | public IDisposable Subscribe(IObserver observer) => _inner.Subscribe(observer);
104 |
105 | private void YieldUntilEmptyOrBlocked()
106 | {
107 | var queue = Interlocked.CompareExchange(ref _nextItems, null!, null!);
108 |
109 | if (queue == null)
110 | {
111 | return;
112 | }
113 |
114 | while (_count < MaximumCount)
115 | {
116 | T next;
117 | lock (queue)
118 | {
119 | if (queue.Count == 0)
120 | {
121 | break;
122 | }
123 |
124 | next = queue.Dequeue();
125 | }
126 |
127 | _inner.OnNext(next);
128 |
129 | if (Interlocked.Increment(ref _count) >= MaximumCount)
130 | {
131 | break;
132 | }
133 | }
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/src/Punchclock/Punchclock.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | netstandard2.0
4 | Punchclock
5 | enable
6 | CS8625;CS8604;CS8600;CS8614;CS8603;CS8618;CS8619
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/Punchclock/ScheduledSubject.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved.
2 | // Licensed to the .NET Foundation under one or more agreements.
3 | // The .NET Foundation licenses this file to you under the MIT license.
4 | // See the LICENSE file in the project root for full license information.
5 |
6 | using System;
7 | using System.Reactive.Concurrency;
8 | using System.Reactive.Disposables;
9 | using System.Reactive.Linq;
10 | using System.Reactive.Subjects;
11 | using System.Threading;
12 |
13 | namespace Punchclock;
14 |
15 | ///
16 | /// A subject which emits using the specified scheduler.
17 | ///
18 | /// The type of item to emit.
19 | internal class ScheduledSubject : ISubject, IDisposable
20 | {
21 | private readonly IObserver? _defaultObserver;
22 | private readonly IScheduler _scheduler;
23 | private readonly Subject _subject = new();
24 |
25 | private int _observerRefCount;
26 | private IDisposable? _defaultObserverSub;
27 | private bool _isDisposed;
28 |
29 | ///
30 | /// Initializes a new instance of the class.
31 | ///
32 | /// The scheduler to emit items on.
33 | /// A default observable which will get values if no other subscribes.
34 | public ScheduledSubject(IScheduler scheduler, IObserver? defaultObserver = null)
35 | {
36 | _scheduler = scheduler;
37 | _defaultObserver = defaultObserver;
38 |
39 | if (defaultObserver != null)
40 | {
41 | _defaultObserverSub = _subject.ObserveOn(_scheduler).Subscribe(_defaultObserver);
42 | }
43 | }
44 |
45 | ///
46 | public void OnCompleted() => _subject.OnCompleted();
47 |
48 | ///
49 | public void OnError(Exception error) => _subject.OnError(error);
50 |
51 | ///
52 | public void OnNext(T value) => _subject.OnNext(value);
53 |
54 | ///
55 | public IDisposable Subscribe(IObserver? observer)
56 | {
57 | if (_defaultObserverSub != null)
58 | {
59 | _defaultObserverSub.Dispose();
60 | _defaultObserverSub = null;
61 | }
62 |
63 | Interlocked.Increment(ref _observerRefCount);
64 |
65 | return new CompositeDisposable(
66 | _subject.ObserveOn(_scheduler).Subscribe(observer),
67 | Disposable.Create(() =>
68 | {
69 | if (Interlocked.Decrement(ref _observerRefCount) <= 0 && _defaultObserver != null)
70 | {
71 | _defaultObserverSub = _subject.ObserveOn(_scheduler).Subscribe(_defaultObserver);
72 | }
73 | }));
74 | }
75 |
76 | ///
77 | public void Dispose()
78 | {
79 | Dispose(true);
80 | GC.SuppressFinalize(this);
81 | }
82 |
83 | ///
84 | /// Disposes managed resources that are disposable and handles cleanup of unmanaged items.
85 | ///
86 | /// If we are disposing managed resources.
87 | protected virtual void Dispose(bool isDisposing)
88 | {
89 | if (_isDisposed)
90 | {
91 | return;
92 | }
93 |
94 | if (isDisposing)
95 | {
96 | _subject?.Dispose();
97 | _defaultObserverSub?.Dispose();
98 | }
99 |
100 | _isDisposed = true;
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/analyzers.ruleset:
--------------------------------------------------------------------------------
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 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
--------------------------------------------------------------------------------
/src/stylecop.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
3 | "settings": {
4 | "indentation": {
5 | "useTabs": false,
6 | "indentationSize": 4
7 | },
8 | "documentationRules": {
9 | "documentExposedElements": true,
10 | "documentInternalElements": false,
11 | "documentPrivateElements": false,
12 | "documentInterfaces": true,
13 | "documentPrivateFields": false,
14 | "documentationCulture": "en-US",
15 | "companyName": ".NET Foundation and Contributors",
16 | "copyrightText": "Copyright (c) 2024 {companyName}. All rights reserved.\nLicensed to the .NET Foundation under one or more agreements.\nThe .NET Foundation licenses this file to you under the {licenseName} license.\nSee the {licenseFile} file in the project root for full license information.",
17 | "variables": {
18 | "licenseName": "MIT",
19 | "licenseFile": "LICENSE"
20 | },
21 | "xmlHeader": false
22 | },
23 | "layoutRules": {
24 | "newlineAtEndOfFile": "allow",
25 | "allowConsecutiveUsings": true
26 | },
27 | "maintainabilityRules": {
28 | "topLevelTypes": [
29 | "class",
30 | "interface",
31 | "struct",
32 | "enum",
33 | "delegate"
34 | ]
35 | },
36 | "orderingRules": {
37 | "usingDirectivesPlacement": "outsideNamespace",
38 | "systemUsingDirectivesFirst": true
39 | },
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/version.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "3.4",
3 | "publicReleaseRefSpec": [
4 | "^refs/heads/main$", // we release out of master
5 | "^refs/heads/preview/.*", // we release previews
6 | "^refs/heads/rel/\\d+\\.\\d+\\.\\d+" // we also release branches starting with rel/N.N.N
7 | ],
8 | "nugetPackageVersion": {
9 | "semVer": 2
10 | },
11 | "cloudBuild": {
12 | "setVersionVariables": true,
13 | "buildNumber": {
14 | "enabled": false
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------