dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.37k stars 9.99k forks source link

Better reconnection logic for Blazor Server #32113

Closed konradbartecki closed 3 months ago

konradbartecki commented 3 years ago

Summary

Currently there is an issue with Blazor Server-side where it is not reconnecting circuits automatically and losing connection especially on mobile browsers.

See #23340 #26985

Motivation and goals

In scope

image

Out of scope

Risks / unknowns

Examples

DefaultReconnectionHandler.ts

 async attemptPeriodicReconnection(options: ReconnectionOptions) {
    for (let i = 0; i < options.maxRetries; i++) {
      this.reconnectDisplay.update(i + 1);

      const delayDuration = i == 0 && options.retryIntervalMilliseconds > ReconnectionProcess.MaximumFirstRetryInterval
                            ? ReconnectionProcess.MaximumFirstRetryInterval
                            : options.retryIntervalMilliseconds;
      this.logger.log(LogLevel.Debug, `Reconnecting in ${delayDuration}`);
      await this.delay(delayDuration);
      //code..
      //try { reconnect() }

     //instead of delaying 3000ms first and trying to reconnect later
     //we could try to reconnect first and delay afterwards
     //this could possibly fix reconnection on suspended mobile browsers

Example onReconnectionFailed event default implementation

    Blazor.defaultReconnectionHandler.onReconnectionFailed = function (d) {
        document.location.reload(); 
    }

I can take care of the implementation of this task.

Related issues

23340

26985

40721

30344

41791

konradbartecki commented 3 years ago

I checked a debug release of my app and my assumptions were correct

On mobile the browser tab is getting awaken from the sleep first we wait the default time of 3000ms then we try to reconnect then this attempt immediately fails because associated state is no longer available on the server (which makes sense)

Solution:

WIP PR: #32122

javiercn commented 3 years ago

@konradbartecki thanks for contacting us.

We have a feature in the works to better support this scenario in 6.0. You can check it here

We recommend you upvote that issue instead so that we can keep track of the interest as well as the issue covering general improvements for server-side Blazor here

javiercn commented 3 years ago

I'm going to close this issue in favor of the two issues mentioned in my previous comment.

Nevermind, I see that there is a PR. We will evaluate it and see if we want to take it.

ghost commented 3 years ago

Thanks for contacting us.

We're moving this issue to the Next sprint planning milestone for future evaluation / consideration. We would like to keep this around to collect more feedback, which can help us with prioritizing this work. We will re-evaluate this issue, during our next planning meeting(s). If we later determine, that the issue has no community involvement, or it's very rare and low-impact issue, we will close it - so that the team can focus on more important and high impact issues. To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.

ghost commented 3 years ago

We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.

HybridSolutions commented 2 years ago

Found some interesting results about this issue. Please check it here

vindberg commented 2 years ago

@konradbartecki is this something we can implement as a quick fix for our Server Blazor applications until the "real" solution is possible (now pushed to V8).

konradbartecki commented 2 years ago

@vindberg Yes, I'm running let's say a custom fork, of dotnet/blazor with this fix only, so you could do it manually by:

See PR: https://github.com/dotnet/aspnetcore/pull/32122

konradbartecki commented 2 years ago

Related:

vindberg commented 2 years ago

Thanks for getting back to me. I see there are some comments from @SteveSandersonMS about the delay and checks (two times ?). Have you included those?

I was hoping for an easier solution that didn't involve a custom fork of the repo. Im not sure MS knows how big an issue this is for Blazor adoption.

konradbartecki commented 1 year ago

Workaround: https://github.com/dotnet/aspnetcore/issues/30344#issuecomment-1292378319

ghost commented 1 year ago

Thanks for contacting us.

We're moving this issue to the .NET 8 Planning milestone for future evaluation / consideration. We would like to keep this around to collect more feedback, which can help us with prioritizing this work. We will re-evaluate this issue, during our next planning meeting(s). If we later determine, that the issue has no community involvement, or it's very rare and low-impact issue, we will close it - so that the team can focus on more important and high impact issues. To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.

mjhillman commented 1 year ago

Can't imagine how many issues have more impact than the web page disconnecting from the server. This is the main reason people prefer not to use Blazor Server.

HybridSolutions commented 1 year ago

Microsoft in general has the ability of ignoring issues that are crucial for the growth of it's platforms and software. This issue with reconnections is a big setback for the user experience. I don't even know who watched a demo of this happening on a screen at .net team offices and said "Wow. That's great! Love that reconnect message and having to press F5 to refresh! Let's make that a feature".

We are using our own script for the reconnection process, at least to make it more user friendly, but this is not a desirable solution.

mserra88 commented 1 year ago

I don't even know who watched a demo of this happening on a screen at .net team offices and said "Wow. That's great! Love that reconnect message and having to press F5 to refresh! Let's make that a feature".

That's just what I was thinking. Is a botchy sloppy solution.

burkjm commented 1 year ago

this reconnect thing ist really annoying. every customer complains about it and somehow it's behaviour is kind of a show stopper.

davidfowl commented 1 year ago

@BrennanConroy we should make sure it’s possible to try out stateful reconnect with blazor server

konradbartecki commented 1 year ago

Guys update for you on how I do it on https://podatki.wtf, seems to be working fine. This automatically reloads the website when the connection is rejected but I still believe that this should be included into the Blazor itself as an opt-in behaviour to ensure backward compatibility.

boot.js

(() => {
    class DefaultReconnectionHandler {
        _currentReconnectionProcess = null

        constructor(overrideDisplay) {
            this._reconnectionDisplay = overrideDisplay
            this._reconnectCallback = Blazor.reconnect;
        }

        onConnectionDown(options, _error) {
            if (!this._reconnectionDisplay) {
                const modal = document.getElementById(options.dialogId)
                this._reconnectionDisplay = new UserSpecifiedDisplay(modal, options.maxRetries, document);
            }
            if (!this._currentReconnectionProcess) {
                this._currentReconnectionProcess = new ReconnectionProcess(
                    options,
                    this._reconnectCallback,
                    this._reconnectionDisplay
                )
            }
        }

        onConnectionUp() {
            if (this._currentReconnectionProcess) {
                this._currentReconnectionProcess.dispose()
                this._currentReconnectionProcess = null
            }
        }
    }

    class ReconnectionProcess {
        isDisposed = false

        constructor(options, reconnectCallback, display) {
            this.reconnectCallback = reconnectCallback
            this.reconnectDisplay = display
            this.reconnectDisplay.show()
            this.attemptPeriodicReconnection(options)
        }

        dispose() {
            this.isDisposed = true
            this.reconnectDisplay.hide()
        }

        async attemptPeriodicReconnection(options) {
            for (let i = 0; i < options.maxRetries; i++) {
                this.reconnectDisplay.update(i + 1)
                if (this.isDisposed) {
                    break
                }
                try {
                    const result = await Blazor.reconnect()
                    if (!result) {
                        // If the server responded and refused to reconnect, reload the page
                        location.reload();
                        return;
                    }
                    return;
                } catch (err) {
                }
                await this.delay(options.retryIntervalMilliseconds)
            }

            location.reload()
        }

        delay(durationMilliseconds) {
            return new Promise(resolve => setTimeout(resolve, durationMilliseconds))
        }
    }

    class UserSpecifiedDisplay {
        static ShowClassName = "components-reconnect-show"

        static HideClassName = "components-reconnect-hide"

        static FailedClassName = "components-reconnect-failed"

        static RejectedClassName = "components-reconnect-rejected"

        static MaxRetriesId = "components-reconnect-max-retries"

        static CurrentAttemptId = "components-reconnect-current-attempt"

        constructor(dialog, maxRetries, document) {
            this.dialog = dialog
            this.maxRetries = maxRetries
            this.document = document
            this.document = document

            const maxRetriesElement = this.document.getElementById(
                UserSpecifiedDisplay.MaxRetriesId
            )

            if (maxRetriesElement) {
                maxRetriesElement.innerText = this.maxRetries.toString()
            }
        }

        show() {
            this.removeClasses()
            this.dialog.classList.add(UserSpecifiedDisplay.ShowClassName)
        }

        update(currentAttempt) {
            const currentAttemptElement = this.document.getElementById(
                UserSpecifiedDisplay.CurrentAttemptId
            )

            if (currentAttemptElement) {
                currentAttemptElement.innerText = currentAttempt.toString()
            }
        }

        hide() {
            this.removeClasses()
            this.dialog.classList.add(UserSpecifiedDisplay.HideClassName)
        }

        failed() {
            this.removeClasses()
            this.dialog.classList.add(UserSpecifiedDisplay.FailedClassName)
        }

        rejected() {
            this.removeClasses()
            this.dialog.classList.add(UserSpecifiedDisplay.RejectedClassName)
        }

        removeClasses() {
            this.dialog.classList.remove(
                UserSpecifiedDisplay.ShowClassName,
                UserSpecifiedDisplay.HideClassName,
                UserSpecifiedDisplay.FailedClassName,
                UserSpecifiedDisplay.RejectedClassName
            )
        }
    }

    Blazor.start({
        reconnectionHandler: new DefaultReconnectionHandler(),
        reconnectionOptions: {
            maxRetries: 6,
            retryIntervalMilliseconds: 3000
        }
    });
})();

Then in the _Layout.cshtml

<script src="_framework/blazor.server.js" autostart="false"></script>
<script src="boot.js"></script>
adamfoneil commented 1 year ago

Would love to see some movement on this. Overall the Blazor Server dev experience is magnificent IMO. But this is pain point for my users. In the app I'm working on, I have vet techs using a tablet during surgeries. They have to pay attention to the veterinarians and what's going on medically. They pick up the tablet running my app, make intermittent entries, and need it to Just Work. They're running into intermittent "disconnects". I'm not sure how to explain what's happening. Maybe I should've gone with WASM for this part of the app, and I know .NET8 will make that Server/WASM mix-n-match more feasible. But anyway -- I'm really hoping we could see some improvement to this user experience here. So much else is awesome in Blazor. Thank you @konradbartecki for that code sample above.

eschimmel commented 1 year ago

The script of @konradbartecki is reconnecting and seems to solve the problem with the circuits breaking the application. I have an app where the user first has to log in and then starts a client side timer, which updates data every minute. After a reconnect I have to make sure to restore the state as it was, including restarting a client side timer. In the first instance this looked somewhat complicated, but was actually simple to solve using ProtectedSessionStorage.

konradbartecki commented 1 year ago

@eschimmel Super glad to hear it's working for you. Now I also believe this should be an opt-in behaviour available in the Blazor framework itself and I've been talking about that for over 2 years. Hopefully dotnet team will merge it.

I think we developer we are aware that circuit might be disposed at any time and it is up to us to save and restore the state later. It is similar to mobile apps where they can be suspended basically at any time and then disposed.

eschimmel commented 1 year ago

@konradbartecki Hi Konrad, it is somewhat difficult to see if it is working or not, but my app records time, using a client side timer and doing a database update every minute. I can let it run now for more than 8 hours without circuits breaking and the timing being precise, whereas that was impossible in the past. I assume that in the background it reconnects, whereas in the past it showed the 'reload' message. If you know how I can see if the circuit gets reconnected, that would help me to investigate it. I couldn't see it in developer tools or fiddler.

konradbartecki commented 1 year ago

You could add console log there '''

           if (!result) {
                    // If the server responded and refused to reconnect, reload the page
                    location.reload();
                    return;

'''

eschimmel commented 1 year ago

@konradbartecki Of course, I will do that and keep you informed if it is reconnecting without problems. I extended the retry interval to 10 seconds.

valterlei-viana commented 1 year ago

magnificent

We have similar problems here in the factory, because the mechanics losing the information all the time, originally from the browser refresh

garrettlondon1 commented 1 year ago

Even if you override the reconnectionHandler in Blazor Server, it will take 15 seconds for the client to even know that it is disconnected!

What's the point of having custom reconnection logic if a user disconnects, doesn't even know they are disconnected, click buttons without anything happening for 15 seconds, for them to only then run into your custom reconnection modal?

Related #48724

konradbartecki commented 1 year ago

@garrettlondon1 interesting, thanks for the PR, I didn't know that was happening! but keep in mind that there is a very similar issue here, where even if we detect that the connection is down, it will take ~5-15s (I don't remember the value used) to even try reconnecting to the server.

Overriding reconnection handler helps with that, as in now when we detect that the connection is down, it will try to reconnect immediately, but you are right, it won't help with the KeepAlive issue you have mentioned. Good catch!

ghost commented 1 year ago

Thanks for contacting us.

We're moving this issue to the .NET 9 Planning milestone for future evaluation / consideration. We would like to keep this around to collect more feedback, which can help us with prioritizing this work. We will re-evaluate this issue, during our next planning meeting(s). If we later determine, that the issue has no community involvement, or it's very rare and low-impact issue, we will close it - so that the team can focus on more important and high impact issues. To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.

HybridSolutions commented 1 year ago

I don't believe we are still dealing with this issue after 2 years. The car is running on 3 wheels but let's postpone a little while longer... This issue is very hard to explain to customers because I don't even believe it myself. Come on Microsoft, deal with the issue. You can do it 😉

frederiktoft commented 11 months ago

We're moving this issue to the .NET 9 Planning milestone

Oh wow, I actually laughed out loud when I read this. What a hilarious joke this has become. A cornerstone feature that many have pointed out for two years is in dire need of fixing (and several have already presented possible fixes), and yet here we are, its yet again pushed to another major version. Yeah, the managers are going to LOVE hearing this piece of news when I present it to them.

jonrmorgan99 commented 10 months ago

The can has been kicked down the road on this one for at least 4 years, see #10325 amongst many others.

Dan Roth's workaround code in that issue worked fine in my apps through V7 but not so well in V8 interactive server mode.

Totally agree that this issue is key to wider Blazor adoption - it just looks like a ghost from Vista !

ghost commented 10 months ago

Thanks for contacting us.

We're moving this issue to the .NET 9 Planning milestone for future evaluation / consideration. We would like to keep this around to collect more feedback, which can help us with prioritizing this work. We will re-evaluate this issue, during our next planning meeting(s). If we later determine, that the issue has no community involvement, or it's very rare and low-impact issue, we will close it - so that the team can focus on more important and high impact issues. To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.

NielsJ-L commented 10 months ago

@konradbartecki Hi Konrad, thank you for sharing the JavaScript (boot.js). Unfortunately, I still get the issue with the page disappearing to white and the < div id="blazor-error-ui" >An error has occurred...< /div >. I am presuming this part of the disconnect/reconnect issue caused by the web browser going into memory-saver/energy-saver when the browser is in the background on the desktop.

I noticed that your website does not have < div id="blazor-error-ui" > tag section, did you find that it was better to remove this to help the JS in boot.js work more effectively?

Have you made other changes that help handle this reconnect issue?

konradbartecki commented 10 months ago

@NielsJ-L I think this is because I use a custom reconnect modal and probably as you did not specify a custom reconnect modal, then Blazor is constructing it's own default custom reconnect modal, so it shows the "error" message.

I have a custom reconnection modal that only shows when there is a connection lost/loading, but does not show when there is an error (because my boot.js script reloads the page when there is an error)

You can check if adding this to your page fixes the issue.

<div id="components-reconnect-modal">
    <div class="modal is-active animate__animated animate__fadeIn animate__delay-1s">
        <div class="modal-background"></div>
        <div class="modal-content is-flex is-justify-content-center">
            <div class="button is-link is-loading is-large has-text"></div>
        </div>
    </div>
</div>

<script src="_framework/blazor.server.js" autostart="false"></script>
<script src="boot.js"></script>

Just a side note that as far as I remember, you need to just add some div with class components-reconnect-modal and then I think the default modal won't be used.

And keep in mind that this code here uses animate.css and Bulma framework, so you will need to adjust it to your app, perhaps maybe starting with a simple div of components-reconnect-modal would be a good way.

My whole boot.js looks as follows:

(() => {
    class DefaultReconnectionHandler {
        _currentReconnectionProcess = null

        constructor(overrideDisplay) {
            this._reconnectionDisplay = overrideDisplay
            this._reconnectCallback = Blazor.reconnect;
        }

        onConnectionDown(options, _error) {
            if (!this._reconnectionDisplay) {
                const modal = document.getElementById(options.dialogId)
                this._reconnectionDisplay = new UserSpecifiedDisplay(modal, options.maxRetries, document);
            }
            if (!this._currentReconnectionProcess) {
                this._currentReconnectionProcess = new ReconnectionProcess(
                    options,
                    this._reconnectCallback,
                    this._reconnectionDisplay
                )
            }
        }

        onConnectionUp() {
            if (this._currentReconnectionProcess) {
                this._currentReconnectionProcess.dispose()
                this._currentReconnectionProcess = null
            }
        }
    }

    class ReconnectionProcess {
        isDisposed = false

        constructor(options, reconnectCallback, display) {
            this.reconnectCallback = reconnectCallback
            this.reconnectDisplay = display
            this.reconnectDisplay.show()
            this.attemptPeriodicReconnection(options)
        }

        dispose() {
            this.isDisposed = true
            this.reconnectDisplay.hide()
        }

        async attemptPeriodicReconnection(options) {
            for (let i = 0; i < options.maxRetries; i++) {
                this.reconnectDisplay.update(i + 1)
                if (this.isDisposed) {
                    break
                }
                try {
                    const result = await Blazor.reconnect()
                    if (!result) {
                        // If the server responded and refused to reconnect, reload the page
                        location.reload();
                        return;
                    }
                    return;
                } catch (err) {
                }
                await this.delay(options.retryIntervalMilliseconds)
            }

            location.reload()
        }

        delay(durationMilliseconds) {
            return new Promise(resolve => setTimeout(resolve, durationMilliseconds))
        }
    }

    class UserSpecifiedDisplay {
        static ShowClassName = "components-reconnect-show"

        static HideClassName = "components-reconnect-hide"

        static FailedClassName = "components-reconnect-failed"

        static RejectedClassName = "components-reconnect-rejected"

        static MaxRetriesId = "components-reconnect-max-retries"

        static CurrentAttemptId = "components-reconnect-current-attempt"

        constructor(dialog, maxRetries, document) {
            this.dialog = dialog
            this.maxRetries = maxRetries
            this.document = document
            this.document = document

            const maxRetriesElement = this.document.getElementById(
                UserSpecifiedDisplay.MaxRetriesId
            )

            if (maxRetriesElement) {
                maxRetriesElement.innerText = this.maxRetries.toString()
            }
        }

        show() {
            this.removeClasses()
            this.dialog.classList.add(UserSpecifiedDisplay.ShowClassName)
        }

        update(currentAttempt) {
            const currentAttemptElement = this.document.getElementById(
                UserSpecifiedDisplay.CurrentAttemptId
            )

            if (currentAttemptElement) {
                currentAttemptElement.innerText = currentAttempt.toString()
            }
        }

        hide() {
            this.removeClasses()
            this.dialog.classList.add(UserSpecifiedDisplay.HideClassName)
        }

        failed() {
            this.removeClasses()
            this.dialog.classList.add(UserSpecifiedDisplay.FailedClassName)
        }

        rejected() {
            this.removeClasses()
            this.dialog.classList.add(UserSpecifiedDisplay.RejectedClassName)
        }

        removeClasses() {
            this.dialog.classList.remove(
                UserSpecifiedDisplay.ShowClassName,
                UserSpecifiedDisplay.HideClassName,
                UserSpecifiedDisplay.FailedClassName,
                UserSpecifiedDisplay.RejectedClassName
            )
        }
    }

    Blazor.start({
        reconnectionHandler: new DefaultReconnectionHandler(),
        reconnectionOptions: {
            maxRetries: 6,
            retryIntervalMilliseconds: 3000
        }
    });
})();
NielsJ-L commented 10 months ago

@konradbartecki Thank you for the quick response. I have been struggling with the connection issue for nearly a month now--I only started working with Blazor 5 months ago. It is really disappointing that Microsoft has developed a framework that IMHO is not ready for prime-time...well, not without a lot work to fix this connection issue.

I appreciate the tip about "components-reconnect-modal" it helped me located a more general version (I'm only using Bootstrap with Blazor in my web app) here: https://stackoverflow.com/questions/58404533/how-can-i-change-the-could-not-reconnect-to-the-server-text-in-blazor

But, now after several minutes the web page goes white and shows the text:

This is the message when attempting to connect to server This is the custom message when failing This is the custom message when refused

This is an improvement, right? 🤣

No, I did not think so either. I confess I am overwhelmed here, I just wanted to learn to write simple Blazor server web app, not fix Microsoft's mistakes.

Thank you again for your help it is greatly appreciated.

garrettlondon1 commented 10 months ago

@konradbartecki Thank you for the quick response. I have been struggling with the connection issue for nearly a month now--I only started working with Blazor 5 months ago. It is really disappointing that Microsoft has developed a framework that IMHO is not ready for prime-time...well, not without a lot work to fix this connection issue.

I appreciate the tip about "components-reconnect-modal" it helped me located a more general version (I'm only using Bootstrap with Blazor in my web app) here: https://stackoverflow.com/questions/58404533/how-can-i-change-the-could-not-reconnect-to-the-server-text-in-blazor

But, now after several minutes the web page goes white and shows the text:

This is the message when attempting to connect to server This is the custom message when failing This is the custom message when refused

This is an improvement, right? 🤣

No, I did not think so either. I confess I am overwhelmed here, I just wanted to learn to write simple Blazor server web app, not fix Microsoft's mistakes.

Thank you again for your help it is greatly appreciated.

What do you mean by not ready for prime-time, the whole application relies on a consistent websocket connection. If the connection is severed, there are no DOM updates able to be transmitted from server. This is the tradeoff that comes with Blazor InteractiveServer versus shipping code to the browser with WASM, or not being able to use interactivity in Blazor SSR.

I don't think your problem has relevance to this issue, the reconnection modal does appear, there is a separate issue I have created for a better experience here: https://github.com/dotnet/aspnetcore/issues/48724

wstaelens commented 9 months ago

We're moving this issue to the .NET 9 Planning milestone

Oh wow, I actually laughed out loud when I read this. What a hilarious joke this has become. A cornerstone feature that many have pointed out for two years is in dire need of fixing (and several have already presented possible fixes), and yet here we are, its yet again pushed to another major version. Yeah, the managers are going to LOVE hearing this piece of news when I present it to them.

sadly but I had to laugh also... Microsoft keeps on doing this for years. . Ignoring seems to be a new strategy.

Johnhersh commented 8 months ago

What do you mean by not ready for prime-time, the whole application relies on a consistent websocket connection. If the connection is severed, there are no DOM updates able to be transmitted from server.

I'm new to Blazor, so this comment is a bit confusing to me. I thought SignalR has a fallback mechanism where it'll go to long-polling, or just HTTP requests, if the web socket connection is broken.

I'm dealing with these reconnection issues as well on mobile or chrome and don't know how to handle them.

This is the tradeoff that comes with Blazor InteractiveServer versus shipping code to the browser with WASM, or not being able to use interactivity in Blazor SSR.

This is another point that confuses me about Blazor. So ok I can switch the whole thing to WASM but then I lose all interactivity. So it requires me to make a whole extra layer of manual http calls. But why? Doesn't SignalR already have a mechanism for communicating without sockets (the same fallback I mention above)? Is it not possible to do whatever SignalR is doing just without web sockets in the case they're not available?

garrettlondon1 commented 8 months ago

What do you mean by not ready for prime-time, the whole application relies on a consistent websocket connection. If the connection is severed, there are no DOM updates able to be transmitted from server.

I'm new to Blazor, so this comment is a bit confusing to me. I thought SignalR has a fallback mechanism where it'll go to long-polling, or just HTTP requests, if the web socket connection is broken.

I'm dealing with these reconnection issues as well on mobile or chrome and don't know how to handle them.

This is the tradeoff that comes with Blazor InteractiveServer versus shipping code to the browser with WASM, or not being able to use interactivity in Blazor SSR.

This is another point that confuses me about Blazor. So ok I can switch the whole thing to WASM but then I lose all interactivity. So it requires me to make a whole extra layer of manual http calls. But why? Doesn't SignalR already have a mechanism for communicating without sockets (the same fallback I mention above)? Is it not possible to do whatever SignalR is doing just without web sockets in the case they're not available?

SignalR does have a fallback mechanism, although it's rarely used for Blazor Server.

  1. Mobile reconnection issues have not been solved to my knowledge yet

  2. Take the "Fetch Data" Example component:

    • With WASM, it works on the client using HTTP Web API calls
    • With InteractiveServer, it calculates the markup on the server and sends it back to the client via SignalR
      • You could actually use Web API here, or go direct to Entity Framework
      • Web API would still not be sent from the client
    • With Static Server Rendering, fetch data will work OnInitialized, but after that, there is no DOM updates without vanilla JS
eschimmel commented 8 months ago

Hi all, I have had problems with the breaking circuits as well. Reading the documentation (https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/handle-errors?view=aspnetcore-8.0) it turns out that almost every unhandled exception breaks the circuit. To prevent my application from breaking I have added try-catches around all the JSRuntime calls. In my case, these call are not essential for the business logic, but they can break the circuit. It doesn't really matter for the functionality if they are called again when failing the first time.

E.g.

public async Task CalculateDate(sbyte offset)
 {
     try
     {
         if (offset != 0)
         {
             Offset += offset;

             Date = await JSRuntime.InvokeAsync<DateTime>("getLocalDate", null);
             Date = Date.AddDays(Offset);

             StateHasChanged();
         }
     }
     catch (Exception)
     {
     }
 }

I also added Seq for local logging to get a better idea of exceptions occuring. Once all the unhandled exceptions were handled the applications became much more stable.

I realize this is not a solution for a better reconnection logic, but it might help you getting a more stable application

Ghevi commented 8 months ago

I'm trying this on a Blazor project with .Net 8 but the onConnectionDown function is never called. This code works in .Net 7 where the first script src is _framework/blazor.server.js instead of _framework/blazor.web.js Am i missing something? Well it seems that using _framework/blazor.server.js works even in .Net 8 and everything seems fine, but i'm not using webassembly or auto rendering mode. The documentation says to use _framework/blazor.web.js in .Net 8...

<script src="_framework/blazor.web.js" autostart="false"></script>
<script>
        Blazor.start({
            reconnectionHandler: {
                onConnectionDown: () => {
                    console.log("connection down")
                },
                onConnectionUp: () => {
                    console.log("connection up")
                },
            },
        });
</script>
TheXenocide commented 7 months ago

It's possible this has been addressed in newer versions as I haven't focused much on reconnect logic recently, but one thing we ran into that presented challenges with reconnection was that the disposal of the scoped service provider was bound to the request lifetime, rather than the circuit lifetime, which results in ObjectDisposedExceptions when reconnecting to a state which has disposable injected services. Ideally, the circuit would keep the scoped service provider undisposed until the circuit is disposed.

danroth27 commented 7 months ago

One UX design issue to consider here (if it hasn't been already mentioned) is to have a reconnection UI that accounts for only part of the page being unresponsive. Currently we overlay the entire page, but it could be that only a single component on the page is using interactive server rendering.

Johnhersh commented 7 months ago

One UX design issue to consider here (if it hasn't been already mentioned) is to have a reconnection UI that accounts for only part of the page being unresponsive. Currently we overlay the entire page, but it could be that only a single component on the page is using interactive server rendering.

I would love to see that!

rhires commented 6 months ago

It might be fun to start working this out as part of a debug session. It's sometimes painful when I'm trying to debug something and the browser loses the connection, which is surprising since it's a known connection and all that's happened is that I've hit a breakpoint but it takes "too long" for the connection to get refreshed, so it just hits that "1 of 8" thing. Like, how does it not know that I'm debugging? Can it figure out how it lost the connection? Or is that a stupid question? It would be useful for it "know" that I'm debugging so that it won't lose the connection.

eschimmel commented 6 months ago

@rhires The problem with debugging is indeed that it might cause a timeout. I partly solved this by adding log statements on the methods. When a method is entered, left and when an exception occurs. Adding these statements manually is tedious. I use the Metalama framework to have it done automatically. Not so long ago, I wrote a few articles about logging and the use of Aspect Oriented Programming and the Metalama framewrok. You can read the articles on my website at: https://byte217.com

mkArtakMSFT commented 3 months ago

This has been completed as part of https://github.com/dotnet/aspnetcore/issues/55721 Another set of improvements in this area is going to be handled separately as part of https://github.com/dotnet/aspnetcore/issues/56678 in the future.