Skip to content

Commit bd4c7ec

Browse files
committed
add: use SO_REUSEPORT on platform supporting it
3 parents 9f28035 + 405075a + 9934a41 commit bd4c7ec

9 files changed

Lines changed: 233 additions & 137 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,17 @@ All notable changes to this project will be documented in this file. From versio
1313
- Optimize requests with `Prefer: count=exact` that do not use ranges or `db-max-rows` by @laurenceisla in #3957
1414
+ Removed unnecessary double count when building the `Content-Range`.
1515
- Add config `client_error_verbosity` to customize error verbosity by @taimoorzaeem in #4088, #3980, #3824
16+
- Use SO_REUSEPORT on platforms supporting it by @mkleczek in #4703 #4694
1617

1718
### Changed
1819

1920
- Log error when `db-schemas` config contains schema `pg_catalog` or `information_schema` by @taimoorzaeem in #4359
2021
+ Now fails at startup. Prior to this, it failed with `PGRST205` on requests related to these schemas.
2122

23+
### Fixed
24+
25+
- Shutdown should wait for in flight requests by @mkleczek in #4702
26+
2227
## [14.6] - 2026-03-06
2328

2429
### Fixed

nix/overlays/haskell-packages.nix

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,53 @@ let
7878
hasql-pool = lib.dontCheck prev.hasql-pool_1_0_1;
7979
hasql-transaction = lib.dontCheck prev.hasql-transaction_1_1_0_1;
8080
postgresql-binary = lib.dontCheck (lib.doJailbreak prev.postgresql-binary_0_13_1_3);
81+
82+
http2 =
83+
prev.callHackageDirect
84+
{
85+
pkg = "http2";
86+
ver = "5.4.0";
87+
sha256 = "sha256-PeEWVd61bQ8G7LvfLeXklzXqNJFaAjE2ecRMWJZESPE=";
88+
}
89+
{ };
90+
91+
http-semantics =
92+
prev.callHackageDirect
93+
{
94+
pkg = "http-semantics";
95+
ver = "0.4.0";
96+
sha256 = "sha256-rh0z51EKvsu5rQd5n2z3fSRjjEObouNZSBPO9NFYOF0=";
97+
}
98+
{ };
99+
100+
network-run =
101+
prev.callHackageDirect
102+
{
103+
pkg = "network-run";
104+
ver = "0.5.0";
105+
sha256 = "sha256-vbXh+CzxDsGApjqHxCYf/ijpZtUCApFbkcF5gyN0THU=";
106+
}
107+
{ };
108+
109+
time-manager =
110+
prev.callHackageDirect
111+
{
112+
pkg = "time-manager";
113+
ver = "0.2.4";
114+
sha256 = "sha256-sAt/331YLQ2IU3z90aKYSq1nxoazv87irsuJp7ZG3pw=";
115+
}
116+
{ };
117+
118+
warp =
119+
lib.dontCheck (prev.callCabal2nixWithOptions "warp"
120+
(super.fetchFromGitHub {
121+
owner = "mkleczek";
122+
repo = "wai";
123+
rev = "e9c9e784dee1b54461b94089175a969eb647d262";
124+
#sha256 = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
125+
sha256 = "sha256-q8vgvsNeKM/fHp3H6+W/tR4HicioqeaU9Eo3rb13bLo=";
126+
}) "--subpath=warp"
127+
{ });
81128
};
82129
in
83130
{

src/PostgREST/Admin.hs

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,23 @@ import PostgREST.Observation (Observation (..))
1919

2020
import qualified PostgREST.AppState as AppState
2121

22-
import Protolude
22+
import qualified Network.Socket as NS
23+
import Protolude
2324

24-
runAdmin :: AppState -> Warp.Settings -> IO ()
25-
runAdmin appState settings = do
26-
whenJust (AppState.getSocketAdmin appState) $ \adminSocket -> do
25+
runAdmin :: AppState -> Maybe NS.Socket -> NS.Socket -> Warp.Settings -> IO ()
26+
runAdmin appState maybeAdminSocket socketREST settings = do
27+
whenJust maybeAdminSocket $ \adminSocket -> do
2728
address <- resolveSocketToAddress adminSocket
2829
observer $ AdminStartObs address
2930
void . forkIO $ Warp.runSettingsSocket settings adminSocket adminApp
3031
where
31-
adminApp = admin appState
32+
adminApp = admin appState socketREST
3233
observer = AppState.getObserver appState
3334

3435
-- | PostgREST admin application
35-
admin :: AppState.AppState -> Wai.Application
36-
admin appState req respond = do
37-
isMainAppReachable <- isRight <$> reachMainApp (AppState.getSocketREST appState)
36+
admin :: AppState.AppState -> NS.Socket -> Wai.Application
37+
admin appState socketREST req respond = do
38+
isMainAppReachable <- isRight <$> reachMainApp socketREST
3839
isLoaded <- AppState.isLoaded appState
3940
isPending <- AppState.isPending appState
4041

@@ -43,8 +44,8 @@ admin appState req respond = do
4344
respond $ Wai.responseLBS (if isMainAppReachable then HTTP.status200 else HTTP.status500) [] mempty
4445
["ready"] ->
4546
let
46-
status | not isMainAppReachable = HTTP.status500
47-
| isPending = HTTP.status503
47+
status | isPending = HTTP.status503
48+
| not isMainAppReachable = HTTP.status500
4849
| isLoaded = HTTP.status200
4950
| otherwise = HTTP.status500
5051
in

src/PostgREST/App.hs

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import GHC.IO.Exception (IOErrorType (..))
2222
import System.IO.Error (ioeGetErrorType)
2323

2424
import Control.Monad.Except (liftEither)
25+
import Control.Monad.Extra (whenJust)
2526
import Data.Either.Combinators (mapLeft, whenLeft)
2627
import Data.Maybe (fromJust)
2728
import Data.String (IsString (..))
@@ -60,31 +61,50 @@ import PostgREST.SchemaCache (SchemaCache (..))
6061
import PostgREST.TimeIt (timeItT)
6162
import PostgREST.Version (docsVersion, prettyVersion)
6263

63-
import qualified Data.ByteString.Char8 as BS
64-
import qualified Data.List as L
65-
import qualified Network.HTTP.Types as HTTP
66-
import Protolude hiding (Handler)
64+
import qualified Data.ByteString.Char8 as BS
65+
import qualified Data.List as L
66+
import Data.Streaming.Network (HostPreference,
67+
bindPortGenEx)
68+
import qualified Data.Text as T
69+
import qualified Network.HTTP.Types as HTTP
70+
import qualified Network.Socket as NS
71+
import PostgREST.Unix (createAndBindDomainSocketNoListen)
72+
import Protolude hiding (Handler)
6773

6874
type Handler = ExceptT Error
6975

7076
run :: AppState -> IO ()
7177
run appState = do
7278
conf@AppConfig{..} <- AppState.getConfig appState
7379

74-
AppState.schemaCacheLoader appState -- Loads the initial SchemaCache
75-
Unix.installSignalHandlers (AppState.getMainThreadId appState) (AppState.schemaCacheLoader appState) (AppState.readInDbConfig False appState)
80+
mainSocket <- initServerSocket conf
81+
adminSocket <- initAdminServerSocket conf
82+
let closeSockets = do
83+
whenJust adminSocket NS.close
84+
NS.close mainSocket
85+
Unix.installSignalHandlers closeSockets (AppState.schemaCacheLoader appState) (AppState.readInDbConfig False appState)
86+
87+
Admin.runAdmin appState adminSocket mainSocket (serverSettings conf)
7688

7789
Listener.runListener appState
7890

79-
Admin.runAdmin appState (serverSettings conf)
91+
-- Kick off and wait for the initial SchemaCache load before listening on
92+
-- the main API socket.
93+
AppState.schemaCacheLoader appState
94+
AppState.waitForSchemaCacheLoaded appState
95+
void $ listenSocket mainSocket
8096

8197
let app = postgrest configLogLevel appState (AppState.schemaCacheLoader appState)
8298

8399
do
84-
address <- resolveSocketToAddress (AppState.getSocketREST appState)
100+
address <- resolveSocketToAddress mainSocket
85101
observer $ AppServerAddressObs address
86102

87-
Warp.runSettingsSocket (serverSettings conf & setOnException onWarpException) (AppState.getSocketREST appState) app
103+
-- Hardcoding maximum graceful shutdown timeout (arbitrary set to 5 seconds)
104+
-- This is unfortunate but necessary becase graceful shutdowns don't work with HTTP keep-alive
105+
-- causing Warp to handle requests on already opened connections even if the listen socket is closed
106+
-- See: https://github.com/yesodweb/wai/issues/853
107+
Warp.runSettingsSocket (serverSettings conf & setOnException onWarpException) mainSocket app
88108
where
89109
observer = AppState.getObserver appState
90110

@@ -229,3 +249,35 @@ addRetryHint delay response = do
229249

230250
isServiceUnavailable :: Wai.Response -> Bool
231251
isServiceUnavailable response = Wai.responseStatus response == HTTP.status503
252+
253+
initServerSocket :: AppConfig -> IO NS.Socket
254+
initServerSocket AppConfig{..} = case configServerUnixSocket of
255+
-- I'm not using `streaming-commons`' bindPath function here because it's not defined for Windows,
256+
-- but we need to have runtime error if we try to use it in Windows, not compile time error
257+
Just path -> createAndBindDomainSocketNoListen path configServerUnixSocketMode
258+
Nothing ->
259+
bindPortTCPWithoutListen configServerPort (fromString $ T.unpack configServerHost)
260+
261+
initAdminServerSocket :: AppConfig -> IO (Maybe NS.Socket)
262+
initAdminServerSocket AppConfig{..} =
263+
traverse (`bindPortTCPWithReusePort` adminHost) configAdminServerPort
264+
where
265+
adminHost = fromString $ T.unpack configAdminServerHost
266+
267+
bindPortTCPWithReusePort :: Int -> HostPreference -> IO NS.Socket
268+
bindPortTCPWithReusePort port hostPreference
269+
= bindPortTCPWithoutListen port hostPreference >>= listenSocket
270+
271+
bindPortTCPWithoutListen :: Int -> HostPreference -> IO NS.Socket
272+
bindPortTCPWithoutListen port hostPreference = do
273+
-- Some unix variants can expose ReusePort but reject it at runtime.
274+
-- Fall back to binding without ReusePort when that happens.
275+
socketWithReusePort <- try (bindPortGenEx reusePortOpts NS.Stream port hostPreference) :: IO (Either SomeException NS.Socket)
276+
either (const $ bindPortGenEx [] NS.Stream port hostPreference) pure socketWithReusePort
277+
where
278+
reusePortOpts = [(NS.ReusePort, 1)]
279+
280+
listenSocket :: NS.Socket -> IO NS.Socket
281+
listenSocket sock = do
282+
NS.listen sock (max 2048 NS.maxListenQueue)
283+
pure sock

0 commit comments

Comments
 (0)