-
-
Notifications
You must be signed in to change notification settings - Fork 299
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
v5 extensions and events design #1820
Comments
I wish to ask that if we are going to use DI for incoming WS events/commands, that could we please create a scope and resolve dependencies through the scope instead? 🙏 |
Yeah i think thats a good request. Question is how we want to scope it, one scope for all handler per event? |
For commands we already have a new di system in our new DSP.Commands package |
imo, if the handlers run concurrently (Task.WhenAll), they should get their own scope. |
they are concurrently |
First thing that come to mind for me is the use of EFCore and its DbContext. It doesn't like playing concurrently. By using a scope per concurrent handler, it would solve this issue for me (most likely others too). |
Could you provide a bit more context on this? What's the issue around them, exactly? |
@Kaoticz Task pretty badly impacts the gateway in terms of allocations (and by extension, speed - event dispatch is pretty badly affected, for example, though there's other bad allocations in there too, as you can probably see) - in my prototyping (which was already a lot faster than the present, abysmal, implementation), swapping out Tasks for ValueTasks yielded a good 5-10% speed and memory footprint improvement, but handling ValueTasks properly makes the code a fair bit more complex (since we don't want to box/AsTask/etc anywhere) and they're easier to break (at least, when people ignore their warnings, which is unfortunately fairly commonplace judging by our support channel) @ecrocombe that's a good point. i've put this on my list of things to invite discussion over. |
If Also, as pointed out by Stephen Toub, This will naturally increase the overall maintenance burden of the library, so you should evaluate if the extra performance is worth the extra complexity. |
Just a few thoughts here: Scoping, if DSharpPlus wants to offer that functionality, should consistently be It would be nice if events had a signature where In addition to what @Kaoticz has said, I would suggest using As far as DI goes, it would be best if DSharpPlus adopted the standard mechanism .NET libraries use for supporting DI (again coming from ASP.NET here) and didn't get in the way of the application defining its own service provider lifetime. This is the hardest hurdle when developers want to also use, say, EF Core cleanly- or run their bot inside of an ASP.NET host. Something like: IServiceCollection ServiceCollectionExtensions.AddDiscordClient(
this IServiceCollection services,
Action<DiscordClientBuilder> configure) Where |
|
As a user of DSharpPlus (not a dev), I have my own ideas on how events should be done. which right now is a mixture of everyone here's opinion. What are everyone's thoughts on using a mediator pattern? Pro's of having our own "EventBus"/"EventPublisher"/"Mediator"
Con's:
Some examples of the mediator pattern can be found: |
i can promise you right here and now that there will not be default configuration for handler concurrency or the ability to unregister handlers. that said, allowing users to override the dispatcher is absolutely a thing we can do under this issue; and if people want to have bad ideas like synchronous handlers, they can themselves |
I'm not sure if you're saying it's not possible, or it's not going to be done under this GitHub issue, either way ill just leave this here as food for thought
I'm not sure unregistering event handlers would be of high demand anyway tbf
Very true, and D#+ can not/will not be responsible for others code, only its own code. |
I think the distinction you have here is important. If the application developer doesn't care/know anything about scope, DSharpPlus is free to decide what
I really just want configurability here- if you want to use scopes, I'd like to be able to configure that use to align with what the other libraries I'm using (and my application logic) expects. @ecrocombe's
I might poke around in the library some more to find a better suggestion, but wouldn't the cached users in |
This only holds true if all services are either
Generally, it's the former, but the library should still take care in not being reckless with scoping, as it's both expensive at runtime, and can cause issues that happen specifically because of how we instantiate these scopes
This is a feasible implementation to have, but muddies dispatch code because of the extra state handling, and having to conditionally create a scope in two different areas. Perhaps this could be delegated to something along the lines of Having it as the default would also bring in to question what the default should in that case
TCS carries a Task object with it, which is a rather heavy cost to incur when it comes to scaling. Request coalescing is something that's more apt to be handled at the higher-level anyway, such as in the ratelimit handler, and even then, is only beneficial if you're making LOTS of requests. Moreover, only one request to a given endpoint (to my knowledge) can proceed at a time to ensure consistent and correct handler behavior, so subsequent requests to the same endpoint would (if enabled) pull from cache |
that isn't happening, not because of library complexity (it's fairly trivial) but because it introduces a pitfall for users where changing a setting may silently break logic without being immediately obvious. we can let users shim the dispatcher if they want to customize scoping.
this is incorrect, requests are by design not synchronized - this would result in very bad caching code and in weird behaviour where a request can sit around queued for a while before pulling from cache. we can try to coalesce near-simultaneous requests, but that's both complex and very limited in what we could do.
how do we know they don't know, under the condition of "no configuration setting"? |
That's acceptable. Shimming the dispatcher should work fine for just about everybody if we can code something functionally equivalent to
You don't really know what they want/need, which is the underlying point I was trying to make- but you could assume that if they haven't shimmed the dispatcher, they'll take whatever it is you're serving without complaint. As it is with all other defaults. |
Description
This is intended to be a bit of an open discussion issue about extensions and events in v5. Of all extensions, this primarily affects Commands and Interactivity, which heavily rely on both the extension and event systems, but when designing, we should keep other extensions in mind.
There are many flaws in the current event system, starting with being able to mutate the event handlers after the gateway starts, which leads to quite some overhead: https://github.com/DSharpPlus/DSharpPlus/blob/master/DSharpPlus/AsyncEvents/AsyncEvent%602.cs#L91-L105
This also includes legacy code such as https://github.com/DSharpPlus/DSharpPlus/blob/master/DSharpPlus/AsyncEvents/AsyncEventArgs.cs#L17 that we should remove, and of course the dreaded
Task
vsValueTask
issue.We have also for a while now envisioned events participating in DI in some way or another, #1683 touches upon that topic and it comes up from time to time in support.
Enlightening the main client about DI now ties into extensions. First, we could see extensions participating in DI, both in their creation and in users being able to request the extension in their own services. The main use-case coming to mind here is naturally Interactivity, but users are only limited by their own creativity here; but this provides significant advantages in creating the extension objects too - from trivially customizing parts of how the extension works, such as
ICommandExecutor
in Commands to error handling using a default-implemented service users can shim individual methods of or replace entirely.Generalizing this concept then begs the question of how to treat DiscordClient. On one hand, DiscordClient (and DiscordShardedClient) are the main entrypoints of any DSharpPlus bot, it is nearly impossible to interact with library code in any meaningful way without using one of those two, on the other hand, they would benefit from participating in DI in much the same ways as extensions, and exposing more components of the library as customizable services would bring a plethora of improvements - caching, ratelimits, the gateway transport layer (for example, supporting ETF as an extension to v5) come to mind.
This would lead us to changing the creation model, away from
DiscordClient..ctor(...)
. There is plenty of design space here, and I'm entirely open to suggestions. I'll pitch the following API for discussion here:static DiscordClient.Create(string token, DiscordIntents intents);
for simplest use-casesstatic DiscordClient.Create(DiscordConfiguration configuration);
for slightly more advanced client-only use-cases (we can consider additional overloads)static DiscordClient.Create(Action<IServiceCollection> configuration);
exposing the underlying service collection, which would allow more complex cases including registering extensions, but wouldn't be particularly clean for complex setupDSharpPlus.DiscordClientBuilder..ctor();
creating a new builder with an empty service collection,DSharpPlus.DiscordClientBuilder..ctor(IServiceCollection);
creating a new builder from a provided service collection,DSharpPlus.DiscordClientBuilder.Build();
creating a DiscordClient instance. We can then expose configuration on the builder, as well as letting extensions provide their own extension methods (for instance, setting the text command prefix could be something likeDiscordClientBuilderExtensions.SetCommandPrefix(this DiscordClientBuilder builder, string prefix);
)Specify the libraries you want this feature request to affect
DSharpPlus, the core library, DSharpPlus.Commands, DSharpPlus.Interactivity
Other considerations
This is a fairly complex task and will most definitely take a while to implement, no matter what design we settle on. There are some preparatory tasks we can already work on, though, such as preparing the default logger to be used in more places and ways, migrating event dispatch to use ValueTask by default (optionally keeping compatibility for Task-based events, though the compiler error here should be obvious enough to let people migrate trivially) and to use immutable event handler collections.
As for design, there are many questions to solve here, starting with the elephant in many rooms - the deprecated extensions. Since they're deprecated, not removed, we'll have to update them to compile and ideally work. Do we care enough to update them to a completely new model or do we find some hacky minimum-effort solution, leaving them dangling between the old and new models? Do we fix the old extension classes up to be able to participate in DI?
I do think this is important to get done one of these days, because changing the extension model marks a huge breaking change for libraries such as Lavalink4NET to adapt to, and the sooner we can stabilize these things for the v5 release cycle, the more painless it will be to update.
As for events, what surface do we want to support? How do we want to provide event handlers with DI? Here, too, is a lot of open design space that will influence almost every bot and almost every extension targeting the library (note that 'almost' is a disclaimer in case there exists one that wouldn't be affected; I cannot provide any examples of either).
From here on, I will be aggregating feedback and the results of design discussions so we can move forward with implementing after we're confident in our design.
The text was updated successfully, but these errors were encountered: