Skip to content

Commit ec196d2

Browse files
committed
Timeout Handling
1 parent 34e8161 commit ec196d2

20 files changed

Lines changed: 300 additions & 80 deletions

NpgsqlRest/Defaults/DefaultCommentParser.cs

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -529,25 +529,23 @@ internal static class DefaultCommentParser
529529
}
530530
}
531531

532-
// command_timeout seconds
533-
// timeout seconds
532+
// command_timeout interval
533+
// timeout interval
534534
else if (haveTag is true && len >= 2 && StrEqualsToArray(wordsLower[0], TimeoutKey))
535535
{
536-
if (int.TryParse(wordsLower[1], out var parsedTimeout))
536+
var parsedInterval = Parser.ParsePostgresInterval(wordsLower[1]);
537+
if (parsedInterval is null)
537538
{
538-
if (routineEndpoint.CommandTimeout != parsedTimeout)
539-
{
540-
if (Options.LogAnnotationSetInfo)
541-
{
542-
Logger?.CommentSetTimeout(description, wordsLower[1]);
543-
}
544-
}
545-
routineEndpoint.CommandTimeout = parsedTimeout;
539+
Logger?.InvalidTimeoutComment(wordsLower[1], description, routineEndpoint.CommandTimeout);
546540
}
547-
else
541+
else if (routineEndpoint.CommandTimeout != parsedInterval)
548542
{
549-
Logger?.InvalidTimeoutComment(wordsLower[1], description, routineEndpoint.CommandTimeout);
543+
if (Options.LogAnnotationSetInfo)
544+
{
545+
Logger?.CommentSetTimeout(description, parsedInterval);
546+
}
550547
}
548+
routineEndpoint.CommandTimeout = parsedInterval;
551549
}
552550

553551
// request_headers_mode [ ignore | context | parameter ]
@@ -1358,6 +1356,15 @@ public static void SetCustomParameter(RoutineEndpoint endpoint, string name, str
13581356
endpoint.ErrorCodePolicy = value;
13591357
}
13601358

1359+
else if (StrEqualsToArray(name, TimeoutKey))
1360+
{
1361+
var parsedInterval = Parser.ParsePostgresInterval(value);
1362+
if (parsedInterval is not null)
1363+
{
1364+
endpoint.CommandTimeout = parsedInterval;
1365+
}
1366+
}
1367+
13611368
else
13621369
{
13631370
if (endpoint.CustomParameters is null)

NpgsqlRest/Defaults/DefaultEndpoint.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ internal static class DefaultEndpoint
2828
method: method,
2929
requestParamType: requestParamType,
3030
requiresAuthorization: Options.RequiresAuthorization,
31-
commandTimeout: Options.CommandTimeout,
3231
responseContentType: null,
3332
responseHeaders: [],
3433
requestHeadersMode: Options.RequestHeadersMode,

NpgsqlRest/Exceptions/NpgsqlToHttpException.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
namespace NpgsqlRest;
44

5-
public class NpgsqlToHttpException(ErrorCodeMappingOptions entry, PostgresException innerException)
6-
: Exception(entry.Title ?? innerException.MessageText, innerException)
5+
public class NpgsqlToHttpException(ErrorCodeMappingOptions entry, NpgsqlException innerException)
6+
: Exception(
7+
entry.Title ?? (innerException is PostgresException exception ? exception.MessageText : innerException.Message), innerException)
78
{
89
public ErrorCodeMappingOptions Mapping { get; } = entry;
910
public string? SqlState { get; } = innerException.SqlState;

NpgsqlRest/Log.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,10 @@ public static partial class Log
4848
public static partial void CommentSetAnon(this ILogger logger, string description);
4949

5050
[LoggerMessage(Level = LogLevel.Debug, Message = "{description} has set COMMAND TIMEOUT by the comment annotation to {parsedTimeout} seconds")]
51-
public static partial void CommentSetTimeout(this ILogger logger, string description, string parsedTimeout);
51+
public static partial void CommentSetTimeout(this ILogger logger, string description, TimeSpan? parsedTimeout);
5252

5353
[LoggerMessage(Level = LogLevel.Warning, Message = "Invalid command timeout '{timeout}' in comment for {description}. Using default command timeout '{defaultTimeout}'")]
54-
public static partial void InvalidTimeoutComment(this ILogger logger, string timeout, string description, int? defaultTimeout);
54+
public static partial void InvalidTimeoutComment(this ILogger logger, string timeout, string description, TimeSpan? defaultTimeout);
5555

5656
[LoggerMessage(Level = LogLevel.Warning, Message = "Invalid request headers mode '{mode}' in comment for {description} Allowed values are Ignore or Context or Parameter. Using default '{defaultRequestHeadersMode}'")]
5757
public static partial void InvalidRequestHeadersModeComment(this ILogger logger, string mode, string description, RequestHeadersMode defaultRequestHeadersMode);
@@ -241,7 +241,7 @@ public static partial class Log
241241

242242
[LoggerMessage(Level = LogLevel.Debug, Message = "Endpoint {urlInfo} enabled rate limiter policy: {policyName}")]
243243
public static partial void EndpointEnabledRateLimiterPolicy(this ILogger logger, string urlInfo, string policyName);
244-
244+
245245
[LoggerMessage(Level = LogLevel.Debug, Message = "{description} has set ERROR CODE POLICY NAME to {name} by the comment annotation.")]
246246
public static partial void ErrorCodePolicySet(this ILogger logger, string description, string name);
247247
}

NpgsqlRest/NpgsqlRestBuilder.cs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,27 +43,32 @@ public static IApplicationBuilder UseNpgsqlRest(this WebApplication builder, Npg
4343

4444
var (
4545
entries,
46-
overloads,
46+
overloads,
4747
hasStreamingEvents
4848
) = Build(builder);
4949
if (entries.Count == 0)
5050
{
5151
return builder;
5252
}
53-
53+
5454
if (hasStreamingEvents is true)
5555
{
5656
builder.UseMiddleware<NpgsqlRestNoticeEventSource>();
5757
}
58-
58+
5959
foreach (var entry in entries)
6060
{
6161
var handler = new NpgsqlRestEndpoint(entry, overloads);
6262
var endpoint = entry.Endpoint;
6363
var methodStr = endpoint.Method.ToString();
6464
var urlInfo = string.Concat(methodStr, " ", endpoint.Path);
6565
var routeBuilder = builder.MapMethods(endpoint.Path, [methodStr], handler.InvokeAsync);
66-
66+
67+
if (options.CommandTimeout is not null && endpoint.CommandTimeout is null)
68+
{
69+
endpoint.CommandTimeout = options.CommandTimeout;
70+
}
71+
6772
if (endpoint.RateLimiterPolicy is not null || options.DefaultRateLimitingPolicy is not null)
6873
{
6974
var policy = endpoint.RateLimiterPolicy ?? options.DefaultRateLimitingPolicy!;

NpgsqlRest/NpgsqlRestEndpoint.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1681,7 +1681,7 @@ private async Task<bool> PrepareCommand(
16811681

16821682
if (endpoint.CommandTimeout.HasValue)
16831683
{
1684-
command.CommandTimeout = endpoint.CommandTimeout.Value;
1684+
command.CommandTimeout = endpoint.CommandTimeout.Value.Seconds;
16851685
}
16861686

16871687
if (Options.CommandCallbackAsync is not null)

NpgsqlRest/Options/ErrorCodeMappingOptions.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
using System.Text;
2-
3-
namespace NpgsqlRest;
1+
namespace NpgsqlRest;
42

53
public class ErrorCodeMappingOptions
64
{
Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
1-
namespace NpgsqlRest
1+
namespace NpgsqlRest;
2+
3+
public class ErrorHandlingOptions
24
{
3-
public class ErrorHandlingOptions
4-
{
5-
public string? DefaultErrorCodePolicy { get; set; } = "Default";
5+
public string? DefaultErrorCodePolicy { get; set; } = "Default";
66

7-
public Dictionary<string, Dictionary<string, ErrorCodeMappingOptions>> ErrorCodePolicies { get; set; } = new()
7+
public Dictionary<string, Dictionary<string, ErrorCodeMappingOptions>> ErrorCodePolicies { get; set; } = new()
8+
{
9+
["Default"] = new()
810
{
9-
["Default"] = new()
10-
{
11-
{ "42501", new() { StatusCode = 403, Title = "Insufficient Privilege" } },
12-
{ "57014", new() { StatusCode = 205, Title = "Cancelled" } },
13-
{ "P0001", new() { StatusCode = 400 } },
14-
{ "P0004", new() { StatusCode = 400 } },
15-
{ "42883", new() { StatusCode = 404, Title = "Not Found" } },
16-
}
17-
};
18-
}
19-
}
11+
{ "42501", new() { StatusCode = 403, Title = "Insufficient Privilege" } },
12+
{ "57014", new() { StatusCode = 205, Title = "Cancelled" } },
13+
{ "P0001", new() { StatusCode = 400 } },
14+
{ "P0004", new() { StatusCode = 400 } },
15+
{ "42883", new() { StatusCode = 404, Title = "Not Found" } },
16+
{ "timeout", new() { StatusCode = 504, Title = "Command execution timed out" } },
17+
}
18+
};
19+
}

NpgsqlRest/Options/NpgsqlRestOptions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ public NpgsqlRestOptions(NpgsqlDataSource dataSource)
197197
/// <summary>
198198
/// Sets the wait time (in seconds) on database commands, before terminating the attempt to execute a command and generating an error. This value when it is not null will override the `NpgsqlCommand` which is 30 seconds. Command timeout property for individual endpoints can be changed with the `EndpointCreated` function callback, or by using comment annotations.
199199
/// </summary>
200-
public int? CommandTimeout { get; set; }
200+
public TimeSpan? CommandTimeout { get; set; }
201201

202202
/// <summary>
203203
/// When not null, forces a method type for all created endpoints. Method types are `GET`, `PUT`, `POST`, `DELETE`, `HEAD`, `OPTIONS`, `TRACE`, `PATCH` or `CONNECT`. When this value is null (default), the method type is always `GET` when the routine volatility option is not volatile or the routine name starts with, `get_`, contains `_get_` or ends with `_get` (case insensitive). Otherwise, it is `POST`. This option for individual endpoints can be changed with the `EndpointCreated` function callback, or by using comment annotations.

NpgsqlRest/Parser.cs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ namespace NpgsqlRest;
55

66
public static partial class Parser
77
{
8-
[GeneratedRegex(@"^(\d*\.?\d+)\s*([a-z]+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase)]
8+
[GeneratedRegex(@"^(\d*\.?\d+)\s*([a-z]*)$", RegexOptions.Compiled | RegexOptions.IgnoreCase)]
99
private static partial Regex IntervalRegex();
1010

11-
public static TimeSpan? ParsePostgresInterval(string interval)
11+
public static TimeSpan? ParsePostgresInterval(string? interval)
1212
{
1313
if (string.IsNullOrWhiteSpace(interval))
1414
{
@@ -17,7 +17,7 @@ public static partial class Parser
1717

1818
interval = interval.Trim().ToLowerInvariant();
1919

20-
// Match number (integer or decimal) followed by optional space and unit
20+
// Match number (integer or decimal) followed by optional space and optional unit
2121
var match = IntervalRegex().Match(interval);
2222
if (!match.Success)
2323
{
@@ -33,13 +33,22 @@ public static partial class Parser
3333
return null;
3434
}
3535

36+
// If no unit provided, default to seconds
37+
if (string.IsNullOrEmpty(unitPart))
38+
{
39+
return TimeSpan.FromSeconds(value);
40+
}
41+
3642
// Map PostgreSQL units to TimeSpan conversions
3743
return unitPart switch
3844
{
45+
"us" or "usec" or "microsecond" or "microseconds" => TimeSpan.FromMicroseconds(value),
46+
"ms" or "msec" or "millisecond" or "milliseconds" => TimeSpan.FromMilliseconds(value),
3947
"s" or "sec" or "second" or "seconds" => TimeSpan.FromSeconds(value),
4048
"m" or "min" or "minute" or "minutes" => TimeSpan.FromMinutes(value),
4149
"h" or "hour" or "hours" => TimeSpan.FromHours(value),
4250
"d" or "day" or "days" => TimeSpan.FromDays(value),
51+
"w" or "week" or "weeks" => TimeSpan.FromDays(value * 7),
4352
_ => null
4453
};
4554
}

0 commit comments

Comments
 (0)