add: zero-downtime restarts with SIGHUP#5112
Conversation
a054dcd to
4dc6577
Compare
a665e6f to
6f8f80d
Compare
6f8f80d to
ab0763a
Compare
This one is based on #5036 and automates the whole process inside PostgREST itself (making #5036 obsolete). With this PR, user is able to restart (without downtime!) PostgREST. So upgrade would be:
It also cooperates with to have zero-downtime restarts (so you can upgrade but also change any configuration value). |
ab0763a to
83a6f3a
Compare
We also have #1517 asking for systemd integration. Looks very interesting but there's a non-trivial amount of code added and to be reviewed. Perhaps we can reuse this package https://hackage.haskell.org/package/systemd-2.3.0 somehow? Or is there a way to introduce systemd integration in a more gradual way? |
|
We should also update this doc https://docs.postgrest.org/en/v14/integrations/systemd.html with this feature. |
I wasn't aware of Having said that, the goal of this PR is not systemd integration by itself, it is here only because restart without notifying systemd about new main process will interfere with systemd managing it. So I implemented basic systemd notification support. |
Having said that, the goal of this PR is not systemd integration by itself, it is here only because restart without notifying systemd about new main process will interfere with systemd managing it. So I implemented basic systemd notification support. |
The most difficulty was not systemd integration as such but with coming up with the right startup sequence and coordination between the parent and child process so that this feature, |
Let's try to do that and see how much code reduction can we get.
It'd be easier to read some docs about the above behavior before trying to review code here, something like a sequence diagram could help. Otherwise it's not easy to understand the logic. |
|
Also check if https://github.com/hercules-ci/warp-systemd helps. TBH I never understood why is systemd socket activation not enough for zero-downtime upgrades for our case. Seems this doesn't need SO_REUSEPORT interaction too. |
Systemd socket activation is not zero-downtime - there is downtime period between old instance stopping listening and new instance starting accepting connections. New requests are queued during this period and the clients either:
To achieve real zero-downtime we need multiple instances running at the same time, so that the new instance is handling traffic before the old one stops listening and gracefully shuts down. Secondly, systemd socket activation is Linux only. This PR implements it on all Posix compliant systems (systemd notifications are optional - lack of systemd environment does not prevent the feature to work). And last but not least: from the point of view of operations or devops teams, having it implemented OOTB in PostgREST simplifies their lives a lot: no need to implement/maintain additional, custom, environment/OS specific scripts/configurations. |
On the second thought - I am not convinced it is worth it. We are talking only a single function, 30 lines of code:
See updated PR description. |
Provide a way to implement zero-downtime upgrades by letting PostgREST start a replacement process and hand traffic over before the old process exits. Install a SIGHUP handler that requests a restart through PostgREST.Process.Restart. The restart path starts the current executable again, waits until the replacement reaches the application ready point, commits the handover, and then stops the old server. Enable the SIGHUP restart handler only when server-reuseport is enabled and both the main and admin servers use TCP sockets. This keeps restart enabled only for configurations where the replacement can bind its listening sockets before the parent shuts down. Integrate with systemd notify by reporting RELOADING=1 during restart and then updating MAINPID together with READY=1 once the replacement process is ready.
83a6f3a to
0a497b3
Compare
@mkleczek I wasn't aware of that at all, could you add some user-facing docs to better understand? |
| @@ -0,0 +1,21 @@ | |||
| {-# LANGUAGE RankNTypes #-} | |||
There was a problem hiding this comment.
This separation of src/library-windows/PostgREST/Process/Restart/Impl.hs and src/library-posix/.. does help a lot in reviewing.
I'm not sure about the Impl.hs naming though. @taimoorzaeem Perhaps you have other suggestions?
Now that the code is better organized into modules it looks easier to review, so not adamant on reusing a library anymore. |
|
|
||
| runReplacementHandover :: ReplacementConfig -> IO a -> IO a | ||
| runReplacementHandover replacementCfg stopAction = do | ||
| withSystemdNotifier $ \notifySystemd -> do |
There was a problem hiding this comment.
Why is systemd inside the library-posix, shouldn't that be in another module?
There was a problem hiding this comment.
Why is systemd inside the
library-posix, shouldn't that be in another module?
It was just too small of a function (and used in only one place) to extract it to a separate module. I'm open to do that if you find it necessary/useful.
| runRestartable :: | ||
| ReplacementConfig -> | ||
| AppRun a -> | ||
| IO a | ||
| runRestartable replacementCfg runApp = do | ||
| bracketOnError | ||
| getChildControl | ||
| (traverse_ closeDuplexChannel) $ | ||
| \childControl -> do | ||
| handoverLock <- newMVar () | ||
| runApp | ||
| (HandoverMode $ isJust childControl) | ||
| (ready replacementCfg childControl handoverLock) | ||
|
|
There was a problem hiding this comment.
Hmm, I don't understand why we need the whole library-posix/ and library-windows separation when all we need to do is just add one preprocessor directive. And then move this module to library/PostgREST.
I haven't yet seen any haskell library do this kind of separation so it seems odd to me. Using the CPP directives is the idiomatic haskell way, unless implementation differences are vast, which doesn't seem to be the case here.
| runRestartable :: | |
| ReplacementConfig -> | |
| AppRun a -> | |
| IO a | |
| runRestartable replacementCfg runApp = do | |
| bracketOnError | |
| getChildControl | |
| (traverse_ closeDuplexChannel) $ | |
| \childControl -> do | |
| handoverLock <- newMVar () | |
| runApp | |
| (HandoverMode $ isJust childControl) | |
| (ready replacementCfg childControl handoverLock) | |
| runRestartable :: | |
| ReplacementConfig -> | |
| AppRun a -> | |
| IO a | |
| #ifndef mingw32_HOST_OS | |
| runRestartable replacementCfg runApp = do | |
| bracketOnError | |
| getChildControl | |
| (traverse_ closeDuplexChannel) $ | |
| \childControl -> do | |
| handoverLock <- newMVar () | |
| runApp | |
| (HandoverMode $ isJust childControl) | |
| (ready replacementCfg childControl handoverLock) | |
| #else | |
| runRestartable _ runApp = | |
| runApp (HandoverMode False) readyUnsupported | |
| where | |
| readyUnsupported _ withRequestReplacement = | |
| withRequestReplacement $ throwIO $ HandoverFailed "Restart handover is not supported on this platform." | |
| #endif |
There was a problem hiding this comment.
Hmm, I don't understand why we need the whole
library-posix/andlibrary-windowsseparation when all we need to do is just add one preprocessor directive.
Preprocessor directives and conditional compilation are evil :)
The differences are more than one function - there are differences in imports as well (unix library is only available on non-windows).
I haven't yet seen any haskell library do this kind of separation so it seems odd to me. Using the CPP directives is the idiomatic haskell way, unless implementation differences are vast, which doesn't seem to be the case here.
I find this way of separating platform specific code much more readable and principled - instead of ad-hoc text inclusion/exclusion that quickly becomes unreadable mess, you have clear separation and visibility into what's platform neutral, what's platform specific and what the platform interface is.
But I am open to changing it to conditional compilation if that's preferred way.
There was a problem hiding this comment.
Conditional compilation has other problems, I believe. Some tooling doesn't work with it nicely. I remember doctests were a problem, but maybe not anymore since they are now changed to run compiled code anyway. I think there was something else as well, but I can't remember what it was.
I like the approach introduced here - but I think we should follow through on it. How about using the same pattern for the existing PostgREST.Unix module - which is a historic relict anyway... now that Windows supports Unix Sockets...
We should probably introduce this pattern for existing code in a separate PR and have the discussion there.
|
I remember the sample bash script that was added on #5036 before and it was a few lines. Frankly, the cost of of maintaining that bash script is looking much smaller than maintaining this amount of code in core.
Yeah but then that's shifting a higher maintenance cost on us, I see some mention of windows being unimplemented in the code; that opens the door for users demanding we implement that later too and deal with edge cases.
I don't think we need that kind of flexibilty, we only need systemd. I'll let other chime in but as it is now this is looking like too much to review and not the right design. |
Yeah, this PR root is actually your suggestion here: #4703 (comment) I think
We already pay the cost of maintaining in-process configuration reloading even though it is imperfect and does not provide full reloading (eg. it is not possible to change log level without restart). It will also never provide a way to turn on GHC metrics in runtime as it requires restart. So one might think of this PR as the ultimate solution for configuration (and schema cache) reloading. As a bonus it allows upgrades.
True.
I don't understand this. This PR is more complex because it implements integration with systemd - without that it would be much simpler (but would not work properly under systemd).
I hear you - let's think about the idea some more and get back to it in the future. |
|
First of all, the idea and feature is fantastic. I believe we should most certainly have this, if we can do it in a maintainable and understandable way. I'm not sure whether that works, but one thing that should be pretty maintainable, I believe, would be to separate this "restart yourself + handover to new process" into a generic library, one that is not PostgREST-specific. One way to do this would be to create an entirely separate Haskell/hackage project, but I believe this would not make it very maintainable in other aspects for us. However, if we can create this generic library as a sub-library in the It would allow us to review, understand and maintain the two different complexities involved here separately: the restart/replacement/handover process itself vs. the handling of (admin) sockets, live/ready state etc. I took the description of the implementation of the PR body and stripped it of everything specific to PostgREST - so the generic algorithm would be: Standalone StartupStandalone Startup
Restart Request
Replacement Startup
Commit
Failure Before Commit
A lot of this seems very generic to me. I have not looked at the code at all, but I would expect the generic interface to be something roughly like:
Having a very simple executable compiled as a test-case for this generic library and run some simple tests with it, would be great - we could even test systemd integration via NixOS tests. On the PostgREST-side we should be able to see the difference between the standalone and restart callbacks easily, to check on the PostgREST-specific logic here. |
It is structured that way - there is a generic There should be no problem with moving the generic module to a separate library. |
|
I fully agree. If we can have the generic parts into a separate library (in tree), review/test that independently and then just plug it into PostgREST as a final step that'd be much better from a maintenance perspective. |
Yeah, I looked a bit a the code now - looking pretty much like what I had in mind :) It's mostly separated, but installing the SIGHUP handler is (naturally) not. This would be needed in the generic library, too, to be able to test this with a very simple binary as-is. It's a bit odd to do in the library, though, because it's not extensible - the handler is replaced (and could be replaced from the application code again, in theory). I think we can deal with this small inconsistency, our "generic" library does not need to be truly generic in the sense of being able to publish this on hackage. But it should be mostly "standalone" in the sense that we can easily test the generic feature entirely in that separate binary. Separating this into a separate library would certainly increase the "let's treat this part as a blackbox" factor for me and make dealing with this easier mentally. |
It's quite small but we can also do the same for postgrest/src/library/PostgREST/Debounce.hs Lines 1 to 19 in 178b1d3 |
Yeah, I remember we discussed to move this to our own "prelude" once we merge |
If going this direction, it would be natural to make the library more of a "generic |
Not opposed to that. We'll still need to install signal handlers from two places, because of SIGUSR1 and SIGUSR2, though. |
Provides a way to implement zero-downtime upgrades by letting PostgREST start a replacement process and hand traffic over before the old process exits.
The idea is to Install a SIGHUP handler that requests a restart. It starts the current executable again, waits until the replacement reaches the application ready point, commits the handover, and then stops the old server.
Enable the SIGHUP restart handler only when
server-reuseportis enabled and both the main and admin servers use TCP sockets. This keeps restart enabled only for configurations where the replacement can bind its listening sockets before the parent shuts down.Restart process is integrated with systemd notify by reporting RELOADING=1 during restart and then updating MAINPID together with READY=1 once the replacement process is ready.
Implemented control flow:
Standalone Startup
Restart Request
Replacement Startup
SO_REUSEPORTthere is no risk because replacement admin server is not started yet.Commit
Failure Before Commit