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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ bld/
[Oo]bj/
[Ll]og/
[Ll]ogs/
publish/

# .NET Core
project.lock.json
Expand Down
94 changes: 93 additions & 1 deletion samples/function-app-service-bus/dotnet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,99 @@ All deployment methods have been fully tested against Azure and the LocalStack f

## Test

Once the resources and function app have been deployed, you can use the [call-http-trigger.sh](./scripts/call-http-trigger.sh) Bash script to invoke the **GetGreetings** HTTP-triggered function. This function returns the most recent greetings stored in the in-memory circular buffer, allowing you to verify that the entire message pipeline is working end to end.
Once the resources and function app have been deployed, you can use the [call-http-trigger.sh](./scripts/call-http-trigger.sh) Bash script to invoke the **GetGreetings** HTTP-triggered function. This function returns the most recent greetings stored in the in-memory circular buffer, allowing you to verify that the entire message pipeline is working end to end. The output should look like this:

```bash
Getting function app name...
Function app [local-func-test] successfully retrieved.
Getting resource group name for function app [local-func-test]...
Resource group [local-rg] successfully retrieved.
Getting the default host name of the function app [local-func-test]...
Function app default host name [local-func-test.azurewebsites.azure.localhost.localstack.cloud:4566] successfully retrieved.
Finding container name with prefix [ls-local-func-test]...
Looking for containers with names starting with [ls-local-func-test]...
Found matching container [ls-local-func-test-tdkqjh]
Container [ls-local-func-test-tdkqjh] found successfully
Getting IP address for container [ls-local-func-test-tdkqjh]...
IP address [172.17.0.7] retrieved successfully for container [ls-local-func-test-tdkqjh]
Getting the host port mapped to internal port 80 in container [ls-local-func-test-tdkqjh]...
Mapped host port [42330] retrieved successfully for container [ls-local-func-test-tdkqjh]
Calling HTTP trigger function to retrieve the last [10] greetings via emulator...
{
"requester": {
"sent": [
"Paolo",
"Max"
]
},
"handler": {
"received": [
"Paolo",
"Max"
],
"sent": [
"Welcome Paolo, glad you're here!",
"Salutations Max, how's everything going?"
]
},
"consumer": {
"received": [
"Welcome Paolo, glad you're here!",
"Salutations Max, how's everything going?"
]
}
}
Calling HTTP trigger function to retrieve the last [10] greetings via container IP address [172.17.0.7]...
{
"requester": {
"sent": [
"Paolo",
"Max"
]
},
"handler": {
"received": [
"Paolo",
"Max"
],
"sent": [
"Welcome Paolo, glad you're here!",
"Salutations Max, how's everything going?"
]
},
"consumer": {
"received": [
"Welcome Paolo, glad you're here!",
"Salutations Max, how's everything going?"
]
}
}
Calling HTTP trigger function to retrieve the last [10] greetings via host port [42330]...
{
"requester": {
"sent": [
"Paolo",
"Max"
]
},
"handler": {
"received": [
"Paolo",
"Max"
],
"sent": [
"Welcome Paolo, glad you're here!",
"Salutations Max, how's everything going?"
]
},
"consumer": {
"received": [
"Welcome Paolo, glad you're here!",
"Salutations Max, how's everything going?"
]
}
}
```

You can also inspect the function app's runtime behavior by viewing the logs of its Docker container. Run `docker logs ls-local-func-test-xxxxxx` (replacing `xxxxxx` with the actual container suffix) to see output similar to the following:

Expand Down
20 changes: 20 additions & 0 deletions samples/function-app-service-bus/dotnet/scripts/deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,26 @@ if [ $DEPLOY -eq 0 ]; then
exit 0
fi

# Check if the application insights az extension is already installed
echo "Checking if [application-insights] az extension is already installed..."
az extension show --name application-insights &>/dev/null

if [[ $? == 0 ]]; then
echo "[application-insights] az extension is already installed"
else
echo "[application-insights] az extension is not installed. Installing..."

# Install application-insights az extension
az extension add --name application-insights 1>/dev/null

if [[ $? == 0 ]]; then
echo "[application-insights] az extension successfully installed"
else
echo "Failed to install [application-insights] az extension"
exit
fi
fi

# Check if the application insights component already exists
echo "Checking if [$APPLICATION_INSIGHTS_NAME] Application Insights component exists in the [$RESOURCE_GROUP_NAME] resource group..."
az monitor app-insights component show \
Expand Down
120 changes: 86 additions & 34 deletions samples/function-app-service-bus/dotnet/src/GreetingFunctions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,13 @@ public class HelloWorld

private static readonly Random _random = new();

// Circular buffer storing the last 100 greetings
private const int MaxGreetingHistory = 100;
private static readonly string[] _greetingHistory = new string[MaxGreetingHistory];
private static int _greetingIndex = 0;
private static int _greetingCount = 0;
private static readonly object _greetingLock = new();
// Circular buffers for message history across all functions
private const int MaxHistory = 100;
private static readonly object _historyLock = new();
private static readonly CircularBuffer _requesterSent = new(MaxHistory);
private static readonly CircularBuffer _handlerReceived = new(MaxHistory);
private static readonly CircularBuffer _handlerSent = new(MaxHistory);
private static readonly CircularBuffer _consumerReceived = new(MaxHistory);

// Static initialization - runs once per application lifetime
private static readonly Lazy<bool> _initialized = new Lazy<bool>(() => { Initialize(); return true; });
Expand Down Expand Up @@ -116,7 +117,7 @@ private static void Initialize()
}
}

/// <summary>
/// <summary>
/// Validates that all required configuration values are present and not empty.
/// With default values in place, only the connection string is mandatory.
/// </summary>
Expand Down Expand Up @@ -214,6 +215,12 @@ private static bool IsConfigurationValid()

_logger.LogInformation("[GreetingHandler] Processing request for name: {name}", requestMessage.Name);

// Record received name in history
lock (_historyLock)
{
_handlerReceived.Add(requestMessage.Name);
}

// Create the response message
var greetingText = GetGreeting(requestMessage.Name);
var outputObj = new ResponseMessage
Expand Down Expand Up @@ -287,6 +294,12 @@ public async Task GreetingRequesterAsync([TimerTrigger("%TIMER_SCHEDULE%", RunOn
_logger.LogInformation("[GreetingRequester] Sending message to input queue '{inputQueue}'...", _inputQueueName);
await sender.SendMessageAsync(serviceBusMessage);
_logger.LogInformation("[GreetingRequester] Successfully sent message to input queue '{inputQueue}' with name: {Name}", _inputQueueName, selectedName);

// Record sent name in history
lock (_historyLock)
{
_requesterSent.Add(selectedName);
}
}
catch (Exception ex)
{
Expand Down Expand Up @@ -320,7 +333,7 @@ public async Task GreetingConsumerAsync([TimerTrigger("%TIMER_SCHEDULE%", RunOnS
}

try
{
{
// Create Service Bus client for receiving messages from the output queue
_logger.LogInformation("[GreetingConsumer] Creating Service Bus client for receiving messages...");
await using var client = _hasClientId && _hasFullyQualifiedNamespace
Expand Down Expand Up @@ -364,6 +377,12 @@ public async Task GreetingConsumerAsync([TimerTrigger("%TIMER_SCHEDULE%", RunOnS

// Complete the message after successful processing
await receiver.CompleteMessageAsync(receivedMessage);

// Record received greeting in history
lock (_historyLock)
{
_consumerReceived.Add(responseMessage.Text);
}
}
else
{
Expand All @@ -387,17 +406,18 @@ public async Task GreetingConsumerAsync([TimerTrigger("%TIMER_SCHEDULE%", RunOnS
finally
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
try
{
await receiver.CloseAsync(cts.Token);
try
{
await receiver.CloseAsync(cts.Token);
}
catch
{ /* timeout or error on close */ }
try
{
await client.DisposeAsync();
}
catch { /* benign */
try
{
await client.DisposeAsync();
}
catch
{ /* benign */
}
}
}
Expand All @@ -421,12 +441,9 @@ private static string GetGreeting(string name)
var template = _greetingTemplates[_random.Next(_greetingTemplates.Length)];
var greeting = string.Format(template, name);

lock (_greetingLock)
lock (_historyLock)
{
_greetingHistory[_greetingIndex] = greeting;
_greetingIndex = (_greetingIndex + 1) % MaxGreetingHistory;
if (_greetingCount < MaxGreetingHistory)
_greetingCount++;
_handlerSent.Add(greeting);
}

return greeting;
Expand All @@ -444,31 +461,66 @@ public async Task<HttpResponseData> GetGreetingsAsync(
[HttpTrigger(AuthorizationLevel.Function, "get", Route = "greetings")] HttpRequestData request,
int count = 20)
{
_logger.LogInformation("[GetGreetings] Retrieving last {count} greetings.", count);
_logger.LogInformation("[GetGreetings] Retrieving last {count} entries.", count);

// Clamp count to valid range
if (count < 1) count = 1;
if (count > MaxGreetingHistory) count = MaxGreetingHistory;
if (count > MaxHistory) count = MaxHistory;

string[] result;
lock (_greetingLock)
object history;
lock (_historyLock)
{
var available = Math.Min(count, _greetingCount);
result = new string[available];

// Read backwards from the most recent entry
for (int i = 0; i < available; i++)
history = new
{
var idx = (_greetingIndex - 1 - i + MaxGreetingHistory) % MaxGreetingHistory;
result[i] = _greetingHistory[idx];
}
requester = new
{
sent = _requesterSent.ToArray(count)
},
handler = new
{
received = _handlerReceived.ToArray(count),
sent = _handlerSent.ToArray(count)
},
consumer = new
{
received = _consumerReceived.ToArray(count)
}
};
}

var response = request.CreateResponse(HttpStatusCode.OK);
response.Headers.Add("Content-Type", "application/json");
await response.WriteStringAsync(JsonSerializer.Serialize(result));
await response.WriteStringAsync(JsonSerializer.Serialize(history));
return response;
}

private sealed class CircularBuffer
{
private readonly string[] _items;
private int _index;
private int _count;

public CircularBuffer(int capacity) => _items = new string[capacity];

public void Add(string item)
{
_items[_index] = item;
_index = (_index + 1) % _items.Length;
if (_count < _items.Length) _count++;
}

public string[] ToArray(int count)
{
var available = Math.Min(count, _count);
var result = new string[available];
for (int i = 0; i < available; i++)
{
var idx = (_index - available + i + _items.Length) % _items.Length;
result[i] = _items[idx];
}
return result;
}
}
}

/// <summary>
Expand Down
Loading