-
Notifications
You must be signed in to change notification settings - Fork 301
Fix command timeout issues in DAB #2912
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,80 @@ | ||||||
| # PostgreSQL Command Timeout Configuration | ||||||
|
|
||||||
| This document describes how to configure command timeout for PostgreSQL data sources in Data API Builder. | ||||||
|
|
||||||
| ## Overview | ||||||
|
|
||||||
| Data API Builder now supports configuring PostgreSQL command timeout through the `command-timeout` option in the data source configuration. This feature allows you to override the default command timeout for all PostgreSQL queries executed by Data API Builder. | ||||||
|
Comment on lines
+5
to
+7
|
||||||
|
|
||||||
| ## Configuration | ||||||
|
|
||||||
| Add the `command-timeout` option to your PostgreSQL data source configuration: | ||||||
|
|
||||||
| ```json | ||||||
| { | ||||||
| "data-source": { | ||||||
| "database-type": "postgresql", | ||||||
| "connection-string": "Host=localhost;Database=mydb;Username=user;Password=pass;", | ||||||
| "options": { | ||||||
| "command-timeout": 60 | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| ### Parameters | ||||||
|
|
||||||
| - **command-timeout**: Integer value representing the timeout in seconds | ||||||
| - **Type**: `integer` | ||||||
| - **Minimum**: `0` | ||||||
| - **Default**: `30` (if not specified) | ||||||
| - **Description**: Sets the wait time (in seconds) before terminating the attempt to execute a command and generating an error | ||||||
|
|
||||||
| ### Behavior | ||||||
|
|
||||||
| 1. **Override**: The `command-timeout` value from the configuration will override any `CommandTimeout` parameter present in the connection string | ||||||
| 2. **Precedence**: Configuration file setting takes priority over connection string setting | ||||||
| 3. **Scope**: Applies to all PostgreSQL queries executed through Data API Builder | ||||||
|
Comment on lines
+33
to
+37
|
||||||
|
|
||||||
| ### Example | ||||||
|
|
||||||
| ```json | ||||||
| { | ||||||
| "$schema": "schemas/dab.draft.schema.json", | ||||||
| "data-source": { | ||||||
| "database-type": "postgresql", | ||||||
| "connection-string": "Host=localhost;Database=bookstore;Username=postgres;Password=password;", | ||||||
| "options": { | ||||||
| "command-timeout": 120 | ||||||
| } | ||||||
| }, | ||||||
| "runtime": { | ||||||
| "rest": { "enabled": true, "path": "/api" }, | ||||||
| "graphql": { "enabled": true, "path": "/graphql" } | ||||||
| }, | ||||||
| "entities": { | ||||||
| "Book": { | ||||||
| "source": { "object": "books", "type": "table" }, | ||||||
| "permissions": [ | ||||||
| { "role": "anonymous", "actions": [{ "action": "*" }] } | ||||||
| ] | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| In this example, all PostgreSQL queries will have a 120-second timeout, regardless of any `CommandTimeout` value in the connection string. | ||||||
|
|
||||||
| ## Implementation Details | ||||||
|
|
||||||
| The feature is implemented by: | ||||||
|
|
||||||
| 1. **Schema Validation**: The JSON schema validates the `command-timeout` parameter | ||||||
| 2. **Options Parsing**: The `PostgreSqlOptions` class parses the timeout value from various data types (integer, string, JsonElement) | ||||||
| 3. **Connection String Processing**: The timeout is applied to the Npgsql connection string builder during connection string normalization | ||||||
| 4. **Override Logic**: Configuration values take precedence over existing connection string parameters | ||||||
|
|
||||||
| ## Related | ||||||
|
|
||||||
| - See `samples/postgresql-command-timeout-example.json` for a complete working example | ||||||
| - For other database types, command timeout can be configured directly in the connection string | ||||||
|
||||||
| - For other database types, command timeout can be configured directly in the connection string | |
| - For other supported database types (such as MSSQL, MySQL, and cosmosdb_postgresql), `command-timeout` can also be configured via the `data-source.options.command-timeout` setting, or via the connection string parameters supported by each provider. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| { | ||
| "$schema": "../schemas/dab.draft.schema.json", | ||
| "data-source": { | ||
| "database-type": "postgresql", | ||
| "connection-string": "Host=localhost;Port=5432;Username=postgres;Password=password;Database=bookshelf;CommandTimeout=120" | ||
| }, | ||
| "runtime": { | ||
| "rest": { | ||
| "enabled": true, | ||
| "path": "/api" | ||
| }, | ||
| "graphql": { | ||
| "enabled": true, | ||
| "path": "/graphql", | ||
| "allow-introspection": true | ||
| }, | ||
| "host": { | ||
| "cors": { | ||
| "origins": ["*"], | ||
| "allow-credentials": false | ||
| }, | ||
| "authentication": { | ||
| "provider": "StaticWebApps" | ||
| }, | ||
| "mode": "development" | ||
| } | ||
| }, | ||
| "entities": { | ||
| "Book": { | ||
| "source": { | ||
| "object": "books", | ||
| "type": "table" | ||
| }, | ||
| "graphql": { | ||
| "enabled": true, | ||
| "type": { | ||
| "singular": "Book", | ||
| "plural": "Books" | ||
| } | ||
| }, | ||
| "rest": { | ||
| "enabled": true | ||
| }, | ||
| "permissions": [ | ||
| { | ||
| "role": "anonymous", | ||
| "actions": [ | ||
| { | ||
| "action": "*" | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| { | ||
| "$schema": "../schemas/dab.draft.schema.json", | ||
| "data-source": { | ||
| "database-type": "postgresql", | ||
| "connection-string": "Host=localhost;Database=bookstore;Username=postgres;Password=password;", | ||
| "options": { | ||
| "command-timeout": 60 | ||
| } | ||
| }, | ||
| "runtime": { | ||
| "rest": { | ||
| "enabled": true, | ||
| "path": "/api" | ||
| }, | ||
| "graphql": { | ||
| "enabled": true, | ||
| "path": "/graphql", | ||
| "allow-introspection": true | ||
| }, | ||
| "host": { | ||
| "cors": { | ||
| "origins": ["*"], | ||
| "allow-credentials": false | ||
| }, | ||
| "authentication": { | ||
| "provider": "StaticWebApps" | ||
| }, | ||
| "mode": "development" | ||
| } | ||
| }, | ||
| "entities": { | ||
| "Book": { | ||
| "source": { | ||
| "object": "books", | ||
| "type": "table" | ||
| }, | ||
| "graphql": { | ||
| "enabled": true, | ||
| "type": { | ||
| "singular": "Book", | ||
| "plural": "Books" | ||
| } | ||
| }, | ||
| "rest": { | ||
| "enabled": true | ||
| }, | ||
| "permissions": [ | ||
| { | ||
| "role": "anonymous", | ||
| "actions": [ | ||
| { | ||
| "action": "*" | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -115,6 +115,12 @@ | |
| "set-session-context": { | ||
| "type": "boolean", | ||
| "description": "Enable sending data to MsSql using session context" | ||
| }, | ||
| "command-timeout": { | ||
| "type": "integer", | ||
| "description": "Command timeout in seconds for database operations", | ||
| "minimum": 0, | ||
| "default": 30 | ||
|
Comment on lines
+119
to
+123
|
||
| } | ||
| } | ||
| } | ||
|
|
@@ -135,7 +141,14 @@ | |
| "options": { | ||
| "type": "object", | ||
| "additionalProperties": false, | ||
| "properties": {}, | ||
| "properties": { | ||
| "command-timeout": { | ||
| "type": "integer", | ||
| "description": "Command timeout in seconds for database operations", | ||
| "minimum": 0, | ||
| "default": 30 | ||
| } | ||
| }, | ||
|
Comment on lines
+144
to
+151
|
||
| "required": [] | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,7 @@ | ||||||||||||||||||||||||||||||||||||||
| // Copyright (c) Microsoft Corporation. | ||||||||||||||||||||||||||||||||||||||
| // Licensed under the MIT License. | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| using System.Text.Json; | ||||||||||||||||||||||||||||||||||||||
| using System.Text.Json.Serialization; | ||||||||||||||||||||||||||||||||||||||
| using Azure.DataApiBuilder.Config.HealthCheck; | ||||||||||||||||||||||||||||||||||||||
| using Azure.DataApiBuilder.Config.NamingPolicies; | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -69,6 +70,12 @@ public int DatasourceThresholdMs | |||||||||||||||||||||||||||||||||||||
| SetSessionContext: ReadBoolOption(namingPolicy.ConvertName(nameof(MsSqlOptions.SetSessionContext)))); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if (typeof(TOptionType).IsAssignableFrom(typeof(PostgreSqlOptions))) | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| return (TOptionType)(object)new PostgreSqlOptions( | ||||||||||||||||||||||||||||||||||||||
| CommandTimeout: ReadIntOption(namingPolicy.ConvertName("command-timeout"))); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| throw new NotSupportedException($"The type {typeof(TOptionType).FullName} is not a supported strongly typed options object"); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
@@ -92,6 +99,52 @@ private bool ReadBoolOption(string option) | |||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| private int? ReadIntOption(string option) | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| if (Options is not null && Options.TryGetValue(option, out object? value)) | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| if (value is int intValue) | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| return intValue; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| else if (value is string stringValue && int.TryParse(stringValue, out int parsedValue)) | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| return parsedValue; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| else if (value is JsonElement jsonElement && jsonElement.ValueKind == JsonValueKind.Number && jsonElement.TryGetInt32(out int jsonValue)) | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| return jsonValue; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+102
to
+121
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||||||||||||||||||
| /// Gets the command timeout value from the options. | ||||||||||||||||||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||||||||||||||||||
| /// <returns>The command timeout in seconds, or 30 (default) if not specified.</returns> | ||||||||||||||||||||||||||||||||||||||
| public int GetCommandTimeout() | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| if (Options is not null && Options.TryGetValue("command-timeout", out object? value)) | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| if (value is int intValue) | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| return intValue; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| else if (value is long longValue && longValue <= int.MaxValue && longValue >= int.MinValue) | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| return (int)longValue; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| else if (value is string stringValue && int.TryParse(stringValue, out int parsedValue)) | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| return parsedValue; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| return 30; // default command timeout | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+129
to
+145
|
||||||||||||||||||||||||||||||||||||||
| if (Options is not null && Options.TryGetValue("command-timeout", out object? value)) | |
| { | |
| if (value is int intValue) | |
| { | |
| return intValue; | |
| } | |
| else if (value is long longValue && longValue <= int.MaxValue && longValue >= int.MinValue) | |
| { | |
| return (int)longValue; | |
| } | |
| else if (value is string stringValue && int.TryParse(stringValue, out int parsedValue)) | |
| { | |
| return parsedValue; | |
| } | |
| } | |
| return 30; // default command timeout | |
| return ReadIntOption("command-timeout") ?? 30; |
Copilot
AI
Oct 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The magic number 30 for default command timeout should be extracted as a named constant to improve maintainability and avoid duplication with the schema default value.
Copilot
AI
Jan 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The MsSqlOptions record should include a CommandTimeout property similar to PostgreSqlOptions. Currently, the schema allows command-timeout for MSSQL (line 119-124 in dab.draft.schema.json), but there's no corresponding property in MsSqlOptions to parse and store it. This needs to be added: public record MsSqlOptions(bool SetSessionContext = true, int? CommandTimeout = null) : IDataSourceOptions;
| public record MsSqlOptions(bool SetSessionContext = true) : IDataSourceOptions; | |
| public record MsSqlOptions(bool SetSessionContext = true, int? CommandTimeout = null) : IDataSourceOptions; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -247,7 +247,7 @@ public static bool TryParseConfig(string json, | |
| } | ||
| else if (ds.DatabaseType is DatabaseType.PostgreSQL && replacementSettings?.DoReplaceEnvVar == true) | ||
| { | ||
| updatedConnection = GetPgSqlConnectionStringWithApplicationName(connectionValue); | ||
| updatedConnection = GetPgSqlConnectionStringWithApplicationName(connectionValue, ds); | ||
|
Comment on lines
247
to
+250
|
||
| } | ||
|
Comment on lines
247
to
251
|
||
|
|
||
| ds = ds with { ConnectionString = updatedConnection }; | ||
|
|
@@ -399,8 +399,9 @@ internal static string GetConnectionStringWithApplicationName(string connectionS | |
| /// else add the Application Name property with DataApiBuilder Application Name based on hosted/oss platform. | ||
| /// </summary> | ||
| /// <param name="connectionString">Connection string for connecting to database.</param> | ||
| /// <returns>Updated connection string with `Application Name` property.</returns> | ||
| internal static string GetPgSqlConnectionStringWithApplicationName(string connectionString) | ||
| /// <param name="dataSource">The data source configuration, used to get command timeout override.</param> | ||
| /// <returns>Updated connection string with `Application Name` property and command timeout.</returns> | ||
| internal static string GetPgSqlConnectionStringWithApplicationName(string connectionString, DataSource? dataSource = null) | ||
|
Comment on lines
399
to
+404
|
||
| { | ||
| // If the connection string is null, empty, or whitespace, return it as is. | ||
| if (string.IsNullOrWhiteSpace(connectionString)) | ||
|
|
@@ -437,6 +438,16 @@ internal static string GetPgSqlConnectionStringWithApplicationName(string connec | |
| connectionStringBuilder.ApplicationName += $",{applicationName}"; | ||
| } | ||
|
|
||
| // Apply command timeout from data source configuration if specified (overrides connection string value) | ||
| if (dataSource?.Options is not null) | ||
| { | ||
| PostgreSqlOptions? pgOptions = dataSource.GetTypedOptions<PostgreSqlOptions>(); | ||
| if (pgOptions?.CommandTimeout is not null) | ||
| { | ||
| connectionStringBuilder.CommandTimeout = pgOptions.CommandTimeout.Value; | ||
| } | ||
| } | ||
|
|
||
| // Return the updated connection string. | ||
| return connectionStringBuilder.ConnectionString; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5795,6 +5795,46 @@ public static EntityPermission GetMinimalPermissionConfig(string roleName) | |
| ); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Test that command timeout is properly read from data source options and applied to connection string. | ||
| /// </summary> | ||
| [TestMethod] | ||
| public void TestCommandTimeoutFromDataSourceOptions() | ||
| { | ||
| // Test SQL Server | ||
| Dictionary<string, object> mssqlOptions = new() | ||
| { | ||
| { "command-timeout", 60 }, | ||
| { "set-session-context", true } | ||
| }; | ||
| DataSource mssqlDataSource = new(DatabaseType.MSSQL, "Server=localhost;Database=test;", mssqlOptions); | ||
| Assert.AreEqual(60, mssqlDataSource.GetCommandTimeout()); | ||
|
|
||
| // Test PostgreSQL | ||
| Dictionary<string, object> pgOptions = new() | ||
| { | ||
| { "command-timeout", 120 } | ||
| }; | ||
| DataSource pgDataSource = new(DatabaseType.PostgreSQL, "Host=localhost;Database=test;", pgOptions); | ||
| Assert.AreEqual(120, pgDataSource.GetCommandTimeout()); | ||
|
|
||
| // Test MySQL | ||
| Dictionary<string, object> mysqlOptions = new() | ||
| { | ||
| { "command-timeout", 45 } | ||
| }; | ||
| DataSource mysqlDataSource = new(DatabaseType.MySQL, "Server=localhost;Database=test;", mysqlOptions); | ||
| Assert.AreEqual(45, mysqlDataSource.GetCommandTimeout()); | ||
|
|
||
| // Test default value when not specified | ||
| DataSource defaultDataSource = new(DatabaseType.MSSQL, "Server=localhost;Database=test;", new()); | ||
| Assert.AreEqual(30, defaultDataSource.GetCommandTimeout()); | ||
|
|
||
| // Test null options | ||
| DataSource nullOptionsDataSource = new(DatabaseType.MSSQL, "Server=localhost;Database=test;", null); | ||
| Assert.AreEqual(30, nullOptionsDataSource.GetCommandTimeout()); | ||
| } | ||
|
Comment on lines
+5802
to
+5836
|
||
|
|
||
| /// <summary> | ||
| /// Reads configuration file for defined environment to acquire the connection string. | ||
| /// CI/CD Pipelines and local environments may not have connection string set as environment variable. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documentation title and content are misleading. This feature is not PostgreSQL-specific - the schema allows command-timeout for MSSQL, PostgreSQL, MySQL, and cosmosdb_postgresql (lines 119-150 in dab.draft.schema.json). The documentation should be generalized to cover all supported database types, not just PostgreSQL. Consider renaming this file to "command-timeout.md" or "database-command-timeout.md".