Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion builds/azure-pipelines/build-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ stages:
workspace:
clean: all

timeoutInMinutes: '120'
timeoutInMinutes: '90'

steps:
- template: 'template-steps-build-test.yml'
Expand Down
7 changes: 7 additions & 0 deletions docs/BindingsOverview.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,13 @@ The delay in milliseconds between processing each batch of changes.

The upper limit on the number of pending changes in the user table that are allowed per application-worker. If the count of changes exceeds this limit, it may result in a scale out. The setting only applies for Azure Function Apps with runtime driven scaling enabled. See the [Scaling](#scaling-for-trigger-bindings) section for more information.

#### WEBSITE_SITE_NAME

The unique name used in creating the lease tables. The local apps depend on this setting for creating unique leases tables, please give a unique name for each app.
> **NOTE:** If the setting is re-used accross apps, having the same function name could cause the functions to use the same lease tables and the function runs to not work as expected.
> **NOTE:** If you have 2 different SQL trigger functions with same functionName locally, not having WEBSITE_SITE_NAME would mean that the same leasees table would be used for both triggers resulting in only one of the functions being triggered.
> **NOTE:** This is a read-only variable that is provided by the azure environment variables for deployed functions and the user provided value will be overridden. Refer to [Environment variables](https://learn.microsoft.com/azure/app-service/reference-app-settings?tabs=kudu%2Cdotnet#app-environment) for apps.

### Scaling for Trigger Bindings

If your application containing functions with SQL trigger bindings is running as an Azure function app, it will be scaled automatically based on the amount of changes that are pending to be processed in the user table. As of today, we only support scaling of function apps running in Elastic Premium plan with 'Runtime Scale Monitoring' enabled. To enable scaling, you will need to go the function app resource's page on Azure Portal, then to Configuration > 'Function runtime settings' and turn on 'Runtime Scale Monitoring'.
Expand Down
1 change: 1 addition & 0 deletions samples/samples-csharp/local.settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "dotnet",
"SqlConnectionString": "",
"WEBSITE_SITE_NAME": "SamplesCSharp",
"Sp_SelectCost": "SelectProductsCost",
"ProductCost": 100
}
Expand Down
1 change: 1 addition & 0 deletions samples/samples-csx/local.settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "dotnet",
"SqlConnectionString": "",
"WEBSITE_SITE_NAME": "SamplesCsx",
"Sp_SelectCost": "SelectProductsCost",
"ProductCost": 100
}
Expand Down
1 change: 1 addition & 0 deletions samples/samples-java/local.settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "java",
"SqlConnectionString": "",
"WEBSITE_SITE_NAME": "SamplesJava",
"Sp_SelectCost": "SelectProductsCost",
"ProductCost": 100
}
Expand Down
3 changes: 2 additions & 1 deletion samples/samples-js-v4/local.settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "node",
"AzureWebJobsFeatureFlags": "EnableWorkerIndexing",
"SqlConnectionString": ""
"SqlConnectionString": "",
"WEBSITE_SITE_NAME": "SamplesNodeV4"
}
}
1 change: 1 addition & 0 deletions samples/samples-js/local.settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "node",
"SqlConnectionString": "",
"WEBSITE_SITE_NAME": "SamplesJavascript",
"Sp_SelectCost": "SelectProductsCost",
"ProductCost": 100
}
Expand Down
1 change: 1 addition & 0 deletions samples/samples-outofproc/local.settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
"SqlConnectionString": "",
"WEBSITE_SITE_NAME": "SamplesOOP",
"Sp_SelectCost": "SelectProductsCost",
"ProductCost": 100
}
Expand Down
1 change: 1 addition & 0 deletions samples/samples-powershell/local.settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"FUNCTIONS_WORKER_RUNTIME": "powershell",
"FUNCTIONS_WORKER_RUNTIME_VERSION" : "~7.2",
"SqlConnectionString": "",
"WEBSITE_SITE_NAME": "SamplesPowershell",
"Sp_SelectCost": "SelectProductsCost",
"ProductCost": 100
}
Expand Down
1 change: 1 addition & 0 deletions samples/samples-python-v2/local.settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"AzureWebJobsFeatureFlags": "EnableWorkerIndexing",
"SqlConnectionString": "",
"WEBSITE_SITE_NAME": "SamplesPythonV2",
"PYTHON_ISOLATE_WORKER_DEPENDENCIES": "1"
}
}
1 change: 1 addition & 0 deletions samples/samples-python/local.settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "python",
"SqlConnectionString": "",
"WEBSITE_SITE_NAME": "SamplesPython",
"Sp_SelectCost": "SelectProductsCost",
"ProductCost": 100
}
Expand Down
8 changes: 7 additions & 1 deletion src/SqlBindingUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,13 @@ public static string GetWebSiteName(IConfiguration configuration)
{
throw new ArgumentNullException(nameof(configuration));
}
return configuration.GetConnectionStringOrSetting(SqlBindingConstants.WEBSITENAME);
string websitename = configuration.GetConnectionStringOrSetting(SqlBindingConstants.WEBSITENAME);
// We require a WEBSITE_SITE_NAME for avoiding duplicates if users use the same function name accross apps.
if (string.IsNullOrEmpty(websitename))
{
throw new ArgumentException($"WEBSITE_SITE_NAME cannot be null or empty in your function app settings, please update the setting with a string value. Please refer to https://github.com/Azure/azure-functions-sql-extension/blob/main/docs/BindingsOverview.md#website_site_name for more information.");
}
return websitename;
}

/// <summary>
Expand Down
3 changes: 2 additions & 1 deletion src/TriggerBinding/SqlTriggerBinding.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@ private string GetUserFunctionId()
string websiteName = SqlBindingUtilities.GetWebSiteName(this._configuration);

var methodInfo = (MethodInfo)this._parameter.Member;
string functionName = $"{methodInfo.DeclaringType.FullName}.{methodInfo.Name}";
// Get the function name from FunctionName attribute for .NET functions and methodInfo.Name for non .Net
string functionName = ((FunctionNameAttribute)methodInfo.GetCustomAttribute(typeof(FunctionNameAttribute)))?.Name ?? $"{methodInfo.Name}";

using (var sha256 = SHA256.Create())
{
Expand Down
26 changes: 15 additions & 11 deletions test/Common/TestUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,9 @@ public static void SetupDatabase(out string MasterConnectionString, out string D

connectionStringBuilder.InitialCatalog = databaseName;

// Set SqlConnectionString env var for the tests to use
// Set SqlConnectionString and WEBSITE_SITE_NAME env variables for the tests to use
Environment.SetEnvironmentVariable("SqlConnectionString", connectionStringBuilder.ToString());
Environment.SetEnvironmentVariable("WEBSITE_SITE_NAME", "TestSqlFunction");
MasterConnectionString = masterConnectionString;
DatabaseName = databaseName;
}
Expand Down Expand Up @@ -357,18 +358,21 @@ public static DataReceivedEventHandler CreateOutputReceievedHandler(TaskCompleti
{
return (object sender, DataReceivedEventArgs e) =>
{
Match match = Regex.Match(e.Data, regex);
if (match.Success)
if (e != null && e.Data != null)
{
// We found the line so now check that the group matches our expected value
string actualValue = match.Groups[1].Value;
if (actualValue == expectedValue)
Match match = Regex.Match(e.Data, regex);
if (match.Success)
{
taskCompletionSource.SetResult(true);
}
else
{
taskCompletionSource.SetException(new Exception($"Expected {valueName} value of {expectedValue} but got value {actualValue}"));
// We found the line so now check that the group matches our expected value
string actualValue = match.Groups[1].Value;
if (actualValue == expectedValue)
{
taskCompletionSource.SetResult(true);
}
else
{
taskCompletionSource.SetException(new Exception($"Expected {valueName} value of {expectedValue} but got value {actualValue}"));
}
}
}
};
Expand Down
63 changes: 63 additions & 0 deletions test/Integration/SqlTriggerBindingIntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -981,5 +981,68 @@ public async Task NewUserFunctionId_Migration_Test()
Assert.True(1 == (int)this.ExecuteScalar($@"SELECT 1 FROM {GlobalStateTableName} WHERE UserFunctionID = N'{userFunctionId}'"), $"{GlobalStateTableName} should have {userFunctionId} row on successful migration");
Assert.True(lastSyncVersion == (long)this.ExecuteScalar($@"SELECT LastSyncVersion FROM {GlobalStateTableName} WHERE UserFunctionID = N'{userFunctionId}'"), $"{GlobalStateTableName} should have {userFunctionId} row woth on successful migration");
}

/// <summary>
/// Ensures that the user function gets invoked for each of the insert, update and delete operation after migration seamlessly.
/// </summary>
[RetryTheory]
[SqlInlineData()]
[UnsupportedLanguages(SupportedLanguages.Java)] // test timing out for Java
public async Task UserFunctionIdMigrationTriggerTest(SupportedLanguages lang)
{
this.SetChangeTrackingForTable("Products");
string userFunctionId = "func-id";
string newUserFuntionId = "new-func-id";
IConfiguration configuration = new ConfigurationBuilder().Build();
var listener = new SqlTriggerListener<Product>(this.DbConnectionString, "dbo.Products", "", userFunctionId, "", Mock.Of<ITriggeredFunctionExecutor>(), Mock.Of<SqlOptions>(), Mock.Of<ILogger>(), configuration);
await listener.StartAsync(CancellationToken.None);
// Cancel immediately so the listener doesn't start processing the changes
await listener.StopAsync(CancellationToken.None);

this.StartFunctionHost(nameof(ProductsTrigger), lang);

int firstId = 1;
int lastId = 30;
await this.WaitForProductChanges(
firstId,
lastId,
SqlChangeOperation.Insert,
() => { this.InsertProducts(firstId, lastId); return Task.CompletedTask; },
id => $"Product {id}",
id => id * 100,
this.GetBatchProcessingTimeout(firstId, lastId));

listener = new SqlTriggerListener<Product>(this.DbConnectionString, "dbo.Products", "", newUserFuntionId, userFunctionId, Mock.Of<ITriggeredFunctionExecutor>(), Mock.Of<SqlOptions>(), Mock.Of<ILogger>(), configuration);
await listener.StartAsync(CancellationToken.None);
// Cancel immediately so the listener doesn't start processing the changes
await listener.StopAsync(CancellationToken.None);

this.StartFunctionHost(nameof(ProductsTrigger), lang);

firstId = 1;
lastId = 20;
// All table columns (not just the columns that were updated) would be returned for update operation.
await this.WaitForProductChanges(
firstId,
lastId,
SqlChangeOperation.Update,
() => { this.UpdateProducts(firstId, lastId); return Task.CompletedTask; },
id => $"Updated Product {id}",
id => id * 100,
this.GetBatchProcessingTimeout(firstId, lastId));

firstId = 11;
lastId = 30;
// The properties corresponding to non-primary key columns would be set to the C# type's default values
// (null and 0) for delete operation.
await this.WaitForProductChanges(
firstId,
lastId,
SqlChangeOperation.Delete,
() => { this.DeleteProducts(firstId, lastId); return Task.CompletedTask; },
_ => null,
_ => 0,
this.GetBatchProcessingTimeout(firstId, lastId));
}
}
}