Skip to content

Commit 2092d2a

Browse files
committed
add: use SO_REUSEPORT on platform supporting it
3 parents 3599134 + c8c75e4 + a4aa58b commit 2092d2a

8 files changed

Lines changed: 154 additions & 84 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: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,20 @@ import qualified PostgREST.AppState as AppState
2222
import qualified Network.Socket as NS
2323
import Protolude
2424

25-
runAdmin :: AppState -> Maybe NS.Socket -> NS.Socket -> Warp.Settings -> IO ()
26-
runAdmin appState maybeAdminSocket socketREST settings = do
25+
runAdmin :: AppState -> Maybe NS.Socket -> IO (Maybe NS.Socket) -> Warp.Settings -> IO ()
26+
runAdmin appState maybeAdminSocket getSocketREST settings = do
2727
whenJust maybeAdminSocket $ \adminSocket -> do
2828
address <- resolveSocketToAddress adminSocket
2929
observer $ AdminStartObs address
3030
void . forkIO $ Warp.runSettingsSocket settings adminSocket adminApp
3131
where
32-
adminApp = admin appState socketREST
32+
adminApp = admin appState getSocketREST
3333
observer = AppState.getObserver appState
3434

3535
-- | PostgREST admin application
36-
admin :: AppState.AppState -> NS.Socket -> Wai.Application
37-
admin appState socketREST req respond = do
38-
isMainAppReachable <- isRight <$> reachMainApp socketREST
36+
admin :: AppState.AppState -> IO (Maybe NS.Socket) -> Wai.Application
37+
admin appState getSocketREST req respond = do
38+
isMainAppReachable <- getSocketREST >>= maybe (pure False) (fmap isRight . reachMainApp)
3939
isLoaded <- AppState.isLoaded appState
4040
isPending <- AppState.isPending appState
4141

@@ -44,8 +44,8 @@ admin appState socketREST req respond = do
4444
respond $ Wai.responseLBS (if isMainAppReachable then HTTP.status200 else HTTP.status500) [] mempty
4545
["ready"] ->
4646
let
47-
status | not isMainAppReachable = HTTP.status500
48-
| isPending = HTTP.status503
47+
status | isPending = HTTP.status503
48+
| not isMainAppReachable = HTTP.status500
4949
| isLoaded = HTTP.status200
5050
| otherwise = HTTP.status500
5151
in

src/PostgREST/App.hs

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

2424
import Control.Monad.Except (liftEither)
25+
import Control.Monad.Extra (whenJust)
26+
import Data.IORef (atomicWriteIORef, newIORef,
27+
readIORef)
2528
import Data.Either.Combinators (mapLeft, whenLeft)
2629
import Data.Maybe (fromJust)
2730
import Data.String (IsString (..))
@@ -62,8 +65,8 @@ import PostgREST.Version (docsVersion, prettyVersion)
6265

6366
import qualified Data.ByteString.Char8 as BS
6467
import qualified Data.List as L
65-
import Data.Streaming.Network (bindPortTCP,
66-
bindRandomPortTCP)
68+
import Data.Streaming.Network (HostPreference,
69+
bindPortGenEx)
6770
import qualified Data.Text as T
6871
import qualified Network.HTTP.Types as HTTP
6972
import qualified Network.Socket as NS
@@ -76,21 +79,36 @@ run :: AppState -> IO ()
7679
run appState = do
7780
conf@AppConfig{..} <- AppState.getConfig appState
7881

79-
AppState.schemaCacheLoader appState -- Loads the initial SchemaCache
80-
(mainSocket, adminSocket) <- initSockets conf
82+
mainSocketRef <- newIORef Nothing
83+
adminSocket <- initAdminServerSocket conf
8184

82-
Unix.installSignalHandlers (AppState.getMainThreadId appState) (AppState.schemaCacheLoader appState) (AppState.readInDbConfig False appState)
85+
let closeSockets = do
86+
whenJust adminSocket NS.close
87+
readIORef mainSocketRef >>= foldMap NS.close
88+
Unix.installSignalHandlers closeSockets (AppState.schemaCacheLoader appState) (AppState.readInDbConfig False appState)
89+
90+
Admin.runAdmin appState adminSocket (readIORef mainSocketRef) (serverSettings conf)
8391

8492
Listener.runListener appState
8593

86-
Admin.runAdmin appState adminSocket mainSocket (serverSettings conf)
94+
-- Kick off and wait for the initial SchemaCache load before creating the
95+
-- main API socket.
96+
AppState.schemaCacheLoader appState
97+
AppState.waitForSchemaCacheLoaded appState
98+
99+
mainSocket <- initServerSocket conf
100+
atomicWriteIORef mainSocketRef $ Just mainSocket
87101

88102
let app = postgrest configLogLevel appState (AppState.schemaCacheLoader appState)
89103

90104
do
91105
address <- resolveSocketToAddress mainSocket
92106
observer $ AppServerAddressObs address
93107

108+
-- Hardcoding maximum graceful shutdown timeout (arbitrary set to 5 seconds)
109+
-- This is unfortunate but necessary becase graceful shutdowns don't work with HTTP keep-alive
110+
-- causing Warp to handle requests on already opened connections even if the listen socket is closed
111+
-- See: https://github.com/yesodweb/wai/issues/853
94112
Warp.runSettingsSocket (serverSettings conf & setOnException onWarpException) mainSocket app
95113
where
96114
observer = AppState.getObserver appState
@@ -237,39 +255,27 @@ addRetryHint delay response = do
237255
isServiceUnavailable :: Wai.Response -> Bool
238256
isServiceUnavailable response = Wai.responseStatus response == HTTP.status503
239257

240-
type AppSockets = (NS.Socket, Maybe NS.Socket)
241-
242-
initSockets :: AppConfig -> IO AppSockets
243-
initSockets AppConfig{..} = do
244-
let
245-
cfg'usp = configServerUnixSocket
246-
cfg'uspm = configServerUnixSocketMode
247-
cfg'host = configServerHost
248-
cfg'port = configServerPort
249-
cfg'adminHost = configAdminServerHost
250-
cfg'adminPort = configAdminServerPort
251-
252-
sock <- case cfg'usp of
253-
-- I'm not using `streaming-commons`' bindPath function here because it's not defined for Windows,
254-
-- but we need to have runtime error if we try to use it in Windows, not compile time error
255-
Just path -> createAndBindDomainSocket path cfg'uspm
256-
Nothing -> do
257-
(_, sock) <-
258-
if cfg'port /= 0
259-
then do
260-
sock <- bindPortTCP cfg'port (fromString $ T.unpack cfg'host)
261-
pure (cfg'port, sock)
262-
else do
263-
-- explicitly bind to a random port, returning bound port number
264-
(num, sock) <- bindRandomPortTCP (fromString $ T.unpack cfg'host)
265-
pure (num, sock)
266-
pure sock
267-
268-
adminSock <- case cfg'adminPort of
269-
Just adminPort -> do
270-
adminSock <- bindPortTCP adminPort (fromString $ T.unpack cfg'adminHost)
271-
pure $ Just adminSock
272-
Nothing -> pure Nothing
273-
274-
pure (sock, adminSock)
275-
258+
initServerSocket :: AppConfig -> IO NS.Socket
259+
initServerSocket AppConfig{..} = case configServerUnixSocket of
260+
-- I'm not using `streaming-commons`' bindPath function here because it's not defined for Windows,
261+
-- but we need to have runtime error if we try to use it in Windows, not compile time error
262+
Just path -> createAndBindDomainSocket path configServerUnixSocketMode
263+
Nothing ->
264+
bindPortTCPWithReusePort configServerPort (fromString $ T.unpack configServerHost)
265+
266+
initAdminServerSocket :: AppConfig -> IO (Maybe NS.Socket)
267+
initAdminServerSocket AppConfig{..} =
268+
traverse (`bindPortTCPWithReusePort` adminHost) configAdminServerPort
269+
where
270+
adminHost = fromString $ T.unpack configAdminServerHost
271+
272+
bindPortTCPWithReusePort :: Int -> HostPreference -> IO NS.Socket
273+
bindPortTCPWithReusePort port hostPreference = do
274+
-- Some unix variants can expose ReusePort but reject it at runtime.
275+
-- Fall back to binding without ReusePort when that happens.
276+
try (bindPortGenEx reusePortOpts NS.Stream port hostPreference) :: IO (Either SomeException NS.Socket)
277+
>>= either (const $ bindPortGenEx [] NS.Stream port hostPreference) pure
278+
>>= listenSocket
279+
where
280+
reusePortOpts = [(NS.ReusePort, 1)]
281+
listenSocket sock = NS.listen sock (max 2048 NS.maxListenQueue) $> sock

src/PostgREST/AppState.hs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ module PostgREST.AppState
2525
, getObserver
2626
, isLoaded
2727
, isPending
28+
, waitForSchemaCacheLoaded
2829
) where
2930

3031
import qualified Data.ByteString.Char8 as BS
@@ -384,6 +385,9 @@ markSchemaCacheLoaded = void . (`tryPutMVar` ()) . getSCStatusMVar . stateSCache
384385
isSchemaCacheLoaded :: AppState -> IO Bool
385386
isSchemaCacheLoaded = fmap not . isEmptyMVar . getSCStatusMVar . stateSCacheStatus
386387

388+
waitForSchemaCacheLoaded :: AppState -> IO ()
389+
waitForSchemaCacheLoaded = void . readMVar . getSCStatusMVar . stateSCacheStatus
390+
387391
-- | Reads the in-db config and reads the config file again
388392
-- | We don't retry reading the in-db config after it fails immediately, because it could have user errors. We just report the error and continue.
389393
readInDbConfig :: Bool -> AppState -> IO ()

src/PostgREST/Unix.hs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,9 @@ import System.Directory (removeFile)
1818
import System.IO.Error (isDoesNotExistError)
1919

2020
-- | Set signal handlers, only for systems with signals
21-
installSignalHandlers :: ThreadId -> IO () -> IO () -> IO ()
21+
installSignalHandlers :: IO () -> IO () -> IO () -> IO ()
2222
#ifndef mingw32_HOST_OS
23-
installSignalHandlers tid usr1 usr2 = do
24-
let interrupt = throwTo tid UserInterrupt
23+
installSignalHandlers interrupt usr1 usr2 = do
2524
install Signals.sigINT interrupt
2625
install Signals.sigTERM interrupt
2726
install Signals.sigUSR1 usr1

test/io/postgrest.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def run(
8888
admin_port=None,
8989
host=None,
9090
wait_for_readiness=True,
91-
wait_max_seconds=1,
91+
wait_max_seconds=3,
9292
no_pool_connection_available=False,
9393
no_startup_stdout=True,
9494
):
@@ -188,6 +188,7 @@ def wait_until_exit(postgrest, timeout=1):
188188
def wait_until_status_code(url, max_seconds, status_code):
189189
"Wait for the given HTTP endpoint to return a status code"
190190
session = requests_unixsocket.Session()
191+
response = None
191192

192193
for _ in range(max_seconds * 10):
193194
try:

0 commit comments

Comments
 (0)