Skip to content

Commit d074eb9

Browse files
committed
add: per request statement timeout using prefer header
WIP Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
1 parent a329bca commit d074eb9

File tree

4 files changed

+48
-11
lines changed

4 files changed

+48
-11
lines changed

src/PostgREST/ApiRequest/Preferences.hs

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
--
77
-- [1] https://datatracker.ietf.org/doc/html/rfc7240
88
--
9-
{-# LANGUAGE NamedFieldPuns #-}
9+
{-# LANGUAGE RecordWildCards #-}
1010
module PostgREST.ApiRequest.Preferences
1111
( Preferences(..)
1212
, PreferCount(..)
@@ -17,6 +17,7 @@ module PostgREST.ApiRequest.Preferences
1717
, PreferTransaction(..)
1818
, PreferTimezone(..)
1919
, PreferMaxAffected(..)
20+
, PreferTimeout(..)
2021
, fromHeaders
2122
, shouldCount
2223
, shouldExplainCount
@@ -43,6 +44,7 @@ import Protolude
4344
-- >>> deriving instance Show PreferHandling
4445
-- >>> deriving instance Show PreferTimezone
4546
-- >>> deriving instance Show PreferMaxAffected
47+
-- >>> deriving instance Show PreferTimeout
4648
-- >>> deriving instance Show Preferences
4749

4850
-- | Preferences recognized by the application.
@@ -56,6 +58,7 @@ data Preferences
5658
, preferHandling :: Maybe PreferHandling
5759
, preferTimezone :: Maybe PreferTimezone
5860
, preferMaxAffected :: Maybe PreferMaxAffected
61+
, preferTimeout :: Maybe PreferTimeout
5962
, invalidPrefs :: [ByteString]
6063
}
6164

@@ -77,6 +80,7 @@ data Preferences
7780
-- ( PreferTimezone "America/Los_Angeles" )
7881
-- , preferMaxAffected = Just
7982
-- ( PreferMaxAffected 100 )
83+
-- , preferTimeout = Nothing
8084
-- , invalidPrefs = []
8185
-- }
8286
--
@@ -93,6 +97,7 @@ data Preferences
9397
-- , preferTimezone = Nothing
9498
-- , preferMaxAffected = Just
9599
-- ( PreferMaxAffected 5999 )
100+
-- , preferTimeout = Nothing
96101
-- , invalidPrefs = [ "invalid" ]
97102
-- }
98103
--
@@ -124,6 +129,7 @@ data Preferences
124129
-- , preferHandling = Just Strict
125130
-- , preferTimezone = Nothing
126131
-- , preferMaxAffected = Nothing
132+
-- , preferTimeout = Nothing
127133
-- , invalidPrefs = [ "anything" ]
128134
-- }
129135
--
@@ -138,7 +144,8 @@ fromHeaders allowTxDbOverride acceptedTzNames headers =
138144
, preferHandling = parsePrefs [Strict, Lenient]
139145
, preferTimezone = if isTimezonePrefAccepted then PreferTimezone <$> timezonePref else Nothing
140146
, preferMaxAffected = PreferMaxAffected <$> maxAffectedPref
141-
, invalidPrefs = filter isUnacceptable prefs
147+
, preferTimeout = PreferTimeout <$> timeoutPref
148+
, invalidPrefs = filter (not . isPrefValid) prefs
142149
}
143150
where
144151
mapToHeadVal :: ToHeaderValue a => [a] -> [ByteString]
@@ -159,10 +166,13 @@ fromHeaders allowTxDbOverride acceptedTzNames headers =
159166
isTimezonePrefAccepted = ((S.member . decodeUtf8 <$> timezonePref) <*> pure acceptedTzNames) == Just True
160167

161168
maxAffectedPref = listStripPrefix "max-affected=" prefs >>= readMaybe . BS.unpack
169+
timeoutPref = listStripPrefix "timeout=" prefs >>= readMaybe . BS.unpack
162170

163-
isUnacceptable p = p `notElem` acceptedPrefs &&
164-
(isNothing (BS.stripPrefix "timezone=" p) || not isTimezonePrefAccepted) &&
165-
isNothing (BS.stripPrefix "max-affected=" p)
171+
isPrefValid p =
172+
p `elem` acceptedPrefs ||
173+
(isJust (BS.stripPrefix "timezone=" p) && isTimezonePrefAccepted) ||
174+
isJust (BS.stripPrefix "max-affected=" p) ||
175+
isJust (BS.stripPrefix "timeout=" p)
166176

167177
parsePrefs :: ToHeaderValue a => [a] -> Maybe a
168178
parsePrefs vals =
@@ -171,8 +181,9 @@ fromHeaders allowTxDbOverride acceptedTzNames headers =
171181
prefMap :: ToHeaderValue a => [a] -> Map.Map ByteString a
172182
prefMap = Map.fromList . fmap (\pref -> (toHeaderValue pref, pref))
173183

184+
174185
prefAppliedHeader :: Preferences -> Maybe HTTP.Header
175-
prefAppliedHeader Preferences {preferResolution, preferRepresentation, preferCount, preferTransaction, preferMissing, preferHandling, preferTimezone, preferMaxAffected } =
186+
prefAppliedHeader Preferences{..} =
176187
if null prefsVals
177188
then Nothing
178189
else Just (HTTP.hPreferenceApplied, combined)
@@ -187,6 +198,7 @@ prefAppliedHeader Preferences {preferResolution, preferRepresentation, preferCou
187198
, toHeaderValue <$> preferHandling
188199
, toHeaderValue <$> preferTimezone
189200
, if preferHandling == Just Strict then toHeaderValue <$> preferMaxAffected else Nothing
201+
, if preferHandling == Just Strict then toHeaderValue <$> preferTimeout else Nothing
190202
]
191203

192204
-- |
@@ -289,3 +301,10 @@ newtype PreferMaxAffected = PreferMaxAffected Int64
289301

290302
instance ToHeaderValue PreferMaxAffected where
291303
toHeaderValue (PreferMaxAffected n) = "max-affected=" <> show n
304+
305+
-- |
306+
-- Statement Timeout per request
307+
newtype PreferTimeout = PreferTimeout Int
308+
309+
instance ToHeaderValue PreferTimeout where
310+
toHeaderValue (PreferTimeout n) = "timeout=" <> show n

src/PostgREST/Query/PreQuery.hs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ import qualified Hasql.DynamicStatements.Snippet as SQL hiding (sql)
1717

1818

1919
import PostgREST.ApiRequest (ApiRequest (..))
20-
import PostgREST.ApiRequest.Preferences (PreferTimezone (..),
20+
import PostgREST.ApiRequest.Preferences (PreferHandling (..),
21+
PreferTimeout (..),
22+
PreferTimezone (..),
2123
Preferences (..))
2224
import PostgREST.Auth.Types (AuthResult (..))
2325
import PostgREST.Config (AppConfig (..))
@@ -35,11 +37,11 @@ import Protolude hiding (Handler)
3537

3638
-- sets transaction variables
3739
txVarQuery :: DbActionPlan -> AppConfig -> AuthResult -> ApiRequest -> SQL.Snippet
38-
txVarQuery dbActPlan AppConfig{..} AuthResult{..} ApiRequest{..} =
40+
txVarQuery dbActPlan AppConfig{..} AuthResult{..} ApiRequest{iPreferences=Preferences{..}, ..} =
3941
-- To ensure `GRANT SET ON PARAMETER <superuser_setting> TO authenticator` works, the role settings must be set before the impersonated role.
4042
-- Otherwise the GRANT SET would have to be applied to the impersonated role. See https://github.com/PostgREST/postgrest/issues/3045
4143
"select " <> intercalateSnippet ", " (
42-
searchPathSql : roleSettingsSql ++ roleSql ++ claimsSql ++ [methodSql, pathSql] ++ headersSql ++ cookiesSql ++ timezoneSql ++ funcSettingsSql ++ appSettingsSql
44+
searchPathSql : roleSettingsSql ++ roleSql ++ claimsSql ++ [methodSql, pathSql] ++ headersSql ++ cookiesSql ++ timezoneSql ++ timeoutSql ++ funcSettingsSql ++ appSettingsSql
4345
)
4446
where
4547
methodSql = setConfigWithConstantName ("request.method", iMethod)
@@ -50,7 +52,11 @@ txVarQuery dbActPlan AppConfig{..} AuthResult{..} ApiRequest{..} =
5052
roleSql = [setConfigWithConstantName ("role", authRole)]
5153
roleSettingsSql = setConfigWithDynamicName <$> HM.toList (fromMaybe mempty $ HM.lookup authRole configRoleSettings)
5254
appSettingsSql = setConfigWithDynamicName . join bimap toUtf8 <$> configAppSettings
53-
timezoneSql = maybe mempty (\(PreferTimezone tz) -> [setConfigWithConstantName ("timezone", tz)]) $ preferTimezone iPreferences
55+
timezoneSql = maybe mempty (\(PreferTimezone tz) -> [setConfigWithConstantName ("timezone", tz)]) preferTimezone
56+
timeoutSql =
57+
case (preferTimeout, preferHandling) of -- only applied on when handling = strict
58+
(Just (PreferTimeout t), Just Strict) -> [setConfigWithConstantName ("statement_timeout", show t <> "s")]
59+
_ -> mempty
5460
funcSettingsSql = setConfigWithDynamicName . join bimap toUtf8 <$> funcSettings
5561
searchPathSql =
5662
let schemas = escapeIdentList (iSchema : configDbExtraSearchPath) in

src/PostgREST/Response.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,4 +299,4 @@ responsePreferences plan ApiRequest{iPreferences=Preferences{..}, iQueryParams=Q
299299
CallReadPlan{} -> preferMaxAffected
300300
_ -> Nothing
301301

302-
in Preferences preferResolution' preferRepresentation' preferCount preferTransaction preferMissing' preferHandling preferTimezone preferMaxAffected' []
302+
in Preferences preferResolution' preferRepresentation' preferCount preferTransaction preferMissing' preferHandling preferTimezone preferMaxAffected' preferTimeout []

test/spec/Feature/Query/PreferencesSpec.hs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,3 +236,15 @@ spec =
236236
`shouldRespondWith`
237237
[json| {"code":"PGRST128","details":null,"hint":null,"message":"Function must return SETOF or TABLE when max-affected preference is used with handling=strict"} |]
238238
{ matchStatus = 400 }
239+
240+
-- Prefer: Timeout
241+
context "test Prefer: timeout with handling=strict" $
242+
it "should fail when timeout is more than the query time" $
243+
request methodGet "/rpc/sleep?seconds=5"
244+
[("Prefer", "timeout=4, handling=strict")]
245+
""
246+
`shouldRespondWith`
247+
[json| {"code":"57014","details":null,"hint":null,"message":"canceling statement due to statement timeout"} |]
248+
{ matchStatus = 500
249+
, matchHeaders = [ matchContentTypeJson ] -- Should this have Preference-Applied?
250+
}

0 commit comments

Comments
 (0)