Add SER308: flag the library's own Wait/WaitAll/TryWait helpers - #3180
Merged
Conversation
mgravell
force-pushed
the
marc/obsolete-sync-wait
branch
from
August 18, 2026 12:35
1e227e7 to
e4c3ac8
Compare
Collaborator
Author
|
For @NickCraver and @philon-msft's consideration: I'm "done" with sync-over-async; thoughts on just saying no, but gently? See also https://seredis.dev/SyncOverAsync |
Wait, WaitAll and TryWait are what SER307 discourages everywhere else, reached through the API the library itself offers for it: the calling thread is held for the round-trip while the reply needs a thread of its own. Shipping SER307 while providing a blessed way to do the same thing was only ever half a position. An analyzer rule rather than [Obsolete], and the reason is entirely about how it is turned off. [Obsolete] reports CS0618, which every obsoletion from every source shares, so a consumer wanting to silence *this* has to silence *all* of them - including deprecations in their own code and in unrelated packages. That was the review feedback, and it is right. ObsoleteAttribute.DiagnosticId exists for exactly this and would be the natural answer, but it is net5+ and this library still targets netstandard2.0: it would give a granular ID on some target frameworks and CS0618 on others. A hybrid was considered and rejected - the attribute would need #if on the interface *and* on ConnectionMultiplexer's own members, since marking only the interface does not warn through the class, and on modern targets both mechanisms would report at the same location. [Experimental] is granular but would be saying something untrue; these APIs are long-standing, not preview. Recorded in the descriptor as worth revisiting rather than settled: if netstandard2.0 and net4x are ever dropped, the attribute becomes strictly better and this rule can retire in its favour. Two unrelated interfaces declare these - IRedisAsync for database, server and subscriber calls, and IConnectionMultiplexer for its own - so both are gathered. Matching goes through FindImplementationForInterfaceMember as well as directly, because ConnectionMultiplexer is what Connect returns, making `conn.Wait(task)` the common shape with the class as its containing type. Testing one interface would have left half the surface unguarded, which is how the first attempt at this missed the multiplexer entirely.
Two kinds, and the distinction is in the comments rather than left to the reader. Most are decorator layers - KeyPrefixed, RedisBase, the Retry* and MultiGroup* wrappers, the test fixture - which must implement the interfaces in full and simply forward; forwarding a flagged member is not using it, and the implementation cannot be dropped while the interface declares it. The exception is CursorEnumerable's synchronous scan path, which is the real thing: it is reached only from a caller who asked for the sync API, and TryWait is what applies the configured timeout to it. Test call sites are suppressed rather than rewritten to await, deliberately: the Wait helpers apply the multiplexer's timeout, which a bare await does not, so rewriting would quietly change what those tests exercise.
mgravell
force-pushed
the
marc/obsolete-sync-wait
branch
from
August 18, 2026 15:09
e4c3ac8 to
288c56a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #3179, reworked after review feedback — the original approach used
[Obsolete]; this one does not. The old commits are gone from the branch; the discussion above is the record of why.Wait,WaitAllandTryWaitare what SER307 discourages everywhere else, reached through the API the library itself offers for it: the calling thread is held for the round-trip while the reply needs a thread of its own to be processed. Shipping SER307 while providing a blessed way to do the same thing was only ever half a position.Why an analyzer rule and not
[Obsolete]The feedback was right:
[Obsolete]reportsCS0618, which every obsoletion from every source shares. A consumer who wants to silence this has to silence all of them, including deprecations in their own code and in unrelated packages. That is far too blunt for an opinion we are choosing to have.Options considered and rejected:
ObsoleteAttribute.DiagnosticId— exists for precisely this, and would be the natural answer. It is net5+, and this library still targets netstandard2.0, so it would give a granular ID on some target frameworks andCS0618on others.#ifon the interface and onConnectionMultiplexer's own members, since marking only the interface does not warn through the class; and on modern targets both mechanisms would report at the same location.[Experimental]— granular, but these APIs are long-standing rather than preview, so it would be saying something untrue.An ID of our own behaves identically on every target framework, sits with the rest of the
SER3xxfamily, and has a docs page. Recorded in the descriptor as worth revisiting rather than settled: if netstandard2.0 and net4x are ever dropped, the attribute becomes strictly better — metadata, no analyzer needed, works in every tool — and SER308 can retire in its favour.Two interfaces, not one
IRedisAsyncdeclares these for database/server/subscriber calls;IConnectionMultiplexerdeclares its own, unrelated set. Both are gathered — testing only one would have left half the surface unguarded, which is exactly how the earlier attempt missed the multiplexer entirely.Matching goes through
FindImplementationForInterfaceMemberas well as directly, becauseConnectionMultiplexeris whatConnectreturns, makingconn.Wait(task)the common shape with the class as the containing type.Suppressions in this repo
Split into its own commit so the rule is reviewable on its own. Two kinds, distinguished in the comments rather than left to the reader:
KeyPrefixed,RedisBase, theRetry*/MultiGroup*wrappers, the test fixture. They must implement the interfaces in full; forwarding a flagged member is not using it, and the implementation cannot be dropped while the interface declares it.CursorEnumerable's synchronous scan path, reached only from a caller who asked for the sync API, whereTryWaitis what applies the configured timeout.Test call sites are suppressed rather than rewritten to
await, deliberately: theWaithelpers apply the multiplexer's timeout and a bareawaitdoes not, so rewriting would quietly change what those tests exercise. Happy to convert them if preferred.Verification
Wait(Task)on someone else's type,Task.Wait(), andManualResetEventSlim.Wait()are all left alone — the rule matches the interface members, not the name.mainon the touched files.