From 98802c22252a5ce6614b040961e4fecb59ccdb49 Mon Sep 17 00:00:00 2001 From: Chad Kittel Date: Wed, 7 Feb 2024 17:54:28 +0000 Subject: [PATCH 01/12] Update example --- ...s-and-filters-image-processing-example.png | Bin 0 -> 36527 bytes docs/patterns/pipes-and-filters-content.md | 199 +++--------------- docs/patterns/pipes-and-filters.yml | 14 +- 3 files changed, 32 insertions(+), 181 deletions(-) create mode 100644 docs/patterns/_images/pipes-and-filters-image-processing-example.png diff --git a/docs/patterns/_images/pipes-and-filters-image-processing-example.png b/docs/patterns/_images/pipes-and-filters-image-processing-example.png new file mode 100644 index 0000000000000000000000000000000000000000..a254c76666145a9131b25731de6a121f454b25e8 GIT binary patch literal 36527 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 literal 0 HcmV?d00001 diff --git a/docs/patterns/pipes-and-filters-content.md b/docs/patterns/pipes-and-filters-content.md index bde8ea3b8b..cf4c773a03 100644 --- a/docs/patterns/pipes-and-filters-content.md +++ b/docs/patterns/pipes-and-filters-content.md @@ -42,7 +42,7 @@ Consider the following points when you decide how to implement this pattern: - **Repeated messages**. If a filter in a pipeline fails after it posts a message to the next stage of the pipeline, another instance of the filter might be run, and it would post a copy of the same message to the pipeline. This scenario could cause two instances of the same message to be passed to the next filter. To avoid this problem, the pipeline should detect and eliminate duplicate messages. - > [!NOTE] + > [!NOTE] > If you implement the pipeline by using message queues (like Azure Service Bus queues), the message queuing infrastructure might provide automatic duplicate message detection and removal. - **Context and state**. In a pipeline, each filter essentially runs in isolation and shouldn't make any assumptions about how it was invoked. Therefore, each filter should be provided with sufficient context to perform its work. This context could include a significant amount of state information. @@ -55,7 +55,7 @@ Use this pattern when: - The processing steps performed by an application have different scalability requirements. - > [!NOTE] + > [!NOTE] > You can group filters that should scale together in the same process. For more information, see the [Compute Resource Consolidation pattern](./compute-resource-consolidation.yml). - You require the flexibility to allow reordering of the processing steps that are performed by an application, or to allow the capability to add and remove steps. @@ -72,189 +72,42 @@ This pattern might not be useful when: ## Example -You can use a sequence of message queues to provide the infrastructure that's required to implement a pipeline. An initial message queue receives unprocessed messages. A component that's implemented as a filter task listens for a message on this queue, performs its work, and then posts the transformed message to the next queue in the sequence. Another filter task can listen for messages on this queue, process them, post the results to another queue, and so on, until the fully transformed data appears in the final message in the queue. This diagram illustrates a pipeline that uses message queues: +You can use a sequence of message queues to provide the infrastructure that's required to implement a pipeline. An initial message queue receives unprocessed messages that becomes the start of the pipes and filters pattern implementation. A component that's implemented as a filter task listens for a message on this queue, performs its work, and then posts a new or transformed message to the next queue in the sequence. Another filter task can listen for messages on this queue, process them, post the results to another queue, and so on, until the final step that ends the pipes and filters process. This diagram illustrates a pipeline that uses message queues: ![Diagram showing a pipeline that uses message queues.](./_images/pipes-and-filters-message-queues.png) -If you're building a solution on Azure, you can use Service Bus queues to provide a reliable and scalable queuing mechanism. The `ServiceBusPipeFilter` class shown in the following C# code demonstrates how you can implement a filter that receives input messages from a queue, processes the messages, and posts the results to another queue. +An image processing pipeline could be implemented using this pattern. If your workload takes an image, the image could pass through a series of largely independent and reorderable filters to perform actions such as content moderation, resizing, watermarking, reorientation, EXIF removal, CDN updates, and so on. In this example, the filters could be implemented as individually deployed Azure Functions or even a single Azure Function app that contains each filter as a isolated function. The use of Azure Function triggers, input bindings, and output bindings can be simplify the filter code and work automatically with a queue-based pipe using a [claim check](./claim-check.yml) to the image to process. -> [!NOTE] -> The `ServiceBusPipeFilter` class is defined in the PipesAndFilters.Shared project, which is available on [GitHub](https://github.com/mspnp/cloud-design-patterns/tree/master/pipes-and-filters). +![Diagram showing the image processing pipeline that uses Azure Queues Storage between a series of Azure Functions to take an image from unprocessed to processed.](./_images/pipes-and-filters-image-processing-example.png) -```csharp -public class ServiceBusPipeFilter -{ - ... - private readonly string inQueuePath; - private readonly string outQueuePath; - ... - private QueueClient inQueue; - private QueueClient outQueue; - ... - - public ServiceBusPipeFilter(..., string inQueuePath, string outQueuePath = null) - { - ... - this.inQueuePath = inQueuePath; - this.outQueuePath = outQueuePath; - } - - public void Start() - { - ... - // Create the outbound filter queue if it doesn't exist. - ... - this.outQueue = QueueClient.CreateFromConnectionString(...); - - ... - // Create the inbound and outbound queue clients. - this.inQueue = QueueClient.CreateFromConnectionString(...); - } - - public void OnPipeFilterMessageAsync( - Func> asyncFilterTask, ...) - { - ... - - this.inQueue.OnMessageAsync( - async (msg) => - { - ... - // Process the filter and send the output to the - // next queue in the pipeline. - var outMessage = await asyncFilterTask(msg); - - // Send the message from the filter processor - // to the next queue in the pipeline. - if (outQueue != null) - { - await outQueue.SendAsync(outMessage); - } - - // Note: There's a chance that the same message could be sent twice - // or that a message could be processed by an upstream or downstream - // filter at the same time. - // This would happen in a situation where processing of a message was - // completed, it was sent to the next pipe/queue, and it then failed - // to complete when using the PeekLock method. - // In a real-world implementation, you should consider idempotent message - // processing and concurrency. - }, - options); - } - - public async Task Close(TimeSpan timespan) - { - // Pause the processing threads. - this.pauseProcessingEvent.Reset(); - - // There's no clean approach for waiting for the threads to complete - // the processing. This example simply stops any new processing, waits - // for the existing thread to complete, closes the message pump, - // and finally returns. - Thread.Sleep(timespan); - - this.inQueue.Close(); - ... - } - - ... -} -``` - -The `Start` method in the `ServiceBusPipeFilter` class connects to a pair of input and output queues, and the `Close` method disconnects from the input queue. The `OnPipeFilterMessageAsync` method performs the actual processing of messages, and the `asyncFilterTask` parameter of this method specifies the processing to be performed. The `OnPipeFilterMessageAsync` method waits for incoming messages on the input queue, runs the code specified by the `asyncFilterTask` parameter over each message as it arrives, and posts the results to the output queue. The queues are specified by the constructor. - -The sample solution implements filters in a set of worker roles. Each worker role can be scaled independently, depending on the complexity of the business processing that it performs or the resources that are required for processing. Additionally, multiple instances of each worker role can be run in parallel to improve throughput. - -The following code shows an Azure worker role named `PipeFilterARoleEntry`, which is defined in the PipeFilterA project in the sample solution. - -```csharp -public class PipeFilterARoleEntry : RoleEntryPoint -{ - ... - private ServiceBusPipeFilter pipeFilterA; - - public override bool OnStart() - { - ... - this.pipeFilterA = new ServiceBusPipeFilter( - ..., - Constants.QueueAPath, - Constants.QueueBPath); - - this.pipeFilterA.Start(); - ... - } - - public override void Run() - { - this.pipeFilterA.OnPipeFilterMessageAsync(async (msg) => - { - // Clone the message and update it. - // Properties set by the broker (Deliver count, enqueue time, ...) - // aren't cloned and must be copied over if required. - var newMsg = msg.Clone(); - - await Task.Delay(500); // DOING WORK - - Trace.TraceInformation("Filter A processed message:{0} at {1}", - msg.MessageId, DateTime.UtcNow); - - newMsg.Properties.Add(Constants.FilterAMessageKey, "Complete"); - - return newMsg; - }); - - ... - } - - ... -} -``` - -This role contains a `ServiceBusPipeFilter` object. The `OnStart` method in the role connects to the queues that receive input messages and post output messages. (The names of the queues are defined in the `Constants` class.) The `Run` method invokes the `OnPipeFilterMessageAsync` method to perform processing on each message that's received. (In this example, the processing is simulated by waiting for a short time.) When processing is complete, a new message is constructed that contains the results (in this case, a custom property is added to the input message), and this message is posted to the output queue. - -The sample code contains another worker role named `PipeFilterBRoleEntry`. It's in the PipeFilterB project. This role is similar to `PipeFilterARoleEntry`, but it performs different processing in the `Run` method. In the example solution, these two roles are combined to construct a pipeline. The output queue for the `PipeFilterARoleEntry` role is the input queue for the `PipeFilterBRoleEntry` role. - -The sample solution also provides two other roles named `InitialSenderRoleEntry` (in the InitialSender project) and `FinalReceiverRoleEntry` (in the FinalReceiver project). The `InitialSenderRoleEntry` role provides the initial message in the pipeline. The `OnStart` method connects to a single queue, and the `Run` method posts a method to that queue. The queue is the input queue that's used by the `PipeFilterARoleEntry` role, so posting a message to it causes the message to be received and processed by the `PipeFilterARoleEntry` role. The processed message then passes through the `PipeFilterBRoleEntry` role. - -The input queue for the `FinalReceiveRoleEntry` role is the output queue for the `PipeFilterBRoleEntry` role. The `Run` method in the `FinalReceiveRoleEntry` role, shown in the following code, receives the message and performs some final processing. It then writes the values of the custom properties added by the filters in the pipeline to the trace output. +Here is an example of what one filter, implemented as an Azure Function, triggered from a Queue Storage pipe with a claim Check to the image, and writing a new claim check to another Queue Storage pipe might look like. More code like this can be found in the [demonstration of the Pipes and Filters pattern](https://github.com/mspnp/cloud-design-patterns/tree/main/pipes-and-filters#readme) available on GitHub. ```csharp -public class FinalReceiverRoleEntry : RoleEntryPoint +// This is the "Resize" filter. It handles message from the pipe named "pipe-xfty", +// performs the resize work, and places a message in the next pipe for anther filter +// to handle. +[Function(nameof(ResizeFilter))] +[QueueOutput("pipe-fjur", Connection = "pipe")] // Destination pipe claim check +public async Task RunAsync( + [QueueTrigger("pipe-xfty", Connection = "pipe")] string imageFilePath, // Source pipe claim check + [BlobInput("{QueueTrigger}", Connection = "pipe")] BlockBlobClient imageBlob, // Image to process from claim check + CancellationToken cancellationToken) { - ... - // Final queue/pipe in the pipeline to process data from. - private ServiceBusPipeFilter queueFinal; + _logger.LogInformation("Processing image {uri} for resizing.", imageBlob.Uri); - public override bool OnStart() + // Idempotency checks + if (await resizeFilterWorkRequiredAsync(imageBlob)) { - ... - // Set up the queue. - this.queueFinal = new ServiceBusPipeFilter(...,Constants.QueueFinalPath); - this.queueFinal.Start(); - ... - } + // Download image based on pipe's queue message body + + // Resize the image - public override void Run() - { - this.queueFinal.OnPipeFilterMessageAsync( - async (msg) => - { - await Task.Delay(500); // DOING WORK - - // The pipeline message was received. - Trace.TraceInformation( - "Pipeline Message Complete - FilterA:{0} FilterB:{1}", - msg.Properties[Constants.FilterAMessageKey], - msg.Properties[Constants.FilterBMessageKey]); - - return null; - }); - ... + // Write resized image back to storage } - ... + // Add location information for in next queue message and place on the next pipe + _logger.LogInformation("Image resizing done or not needed. Adding image \"{filePath}\" into the next pipe.", imageFilePath); + return imageFilePath; } ``` @@ -262,7 +115,7 @@ public class FinalReceiverRoleEntry : RoleEntryPoint You might find the following resources helpful when you implement this pattern: -- [A sample that demonstrates this pattern, on GitHub](https://github.com/mspnp/cloud-design-patterns/tree/master/pipes-and-filters) +- A [demonstration of the Pipes and Filters Pattern](https://github.com/mspnp/cloud-design-patterns/tree/main/pipes-and-filters#readme) using the image processing scenario is available on GitHub. - [Idempotency patterns](https://blog.jonathanoliver.com/idempotency-patterns), on Jonathan Oliver's blog ## Related resources diff --git a/docs/patterns/pipes-and-filters.yml b/docs/patterns/pipes-and-filters.yml index b4e2db170d..158f5ae59d 100644 --- a/docs/patterns/pipes-and-filters.yml +++ b/docs/patterns/pipes-and-filters.yml @@ -1,19 +1,17 @@ ### YamlMime:Architecture metadata: title: Pipes and Filters pattern - description: Break down a task that performs complex processing into a series of separate elements that can be reused. + description: Break down a task that performs complex processing into a series of separate elements that can be reused or reordered. author: martinekuan ms.author: architectures ms.date: 07/28/2022 - ms.topic: conceptual + ms.topic: design-pattern ms.service: architecture-center - ms.subservice: azure-guide + ms.subservice: design-pattern azureCategories: - - devops -products: - - azure-devops + - compute name: Pipes and Filters pattern -summary: Break down a task that performs complex processing into a series of separate elements that can be reused. +summary: Break down a task that performs complex processing into a series of separate elements that can be reused or reordered. thumbnailUrl: /azure/architecture/browse/thumbs/pipes-filters-solution.png content: | - [!include[](pipes-and-filters-content.md)] + [!INCLUDE[](pipes-and-filters-content.md)] From dde7860c6282fe99d3a2a1f33e00bff615d3fc6e Mon Sep 17 00:00:00 2001 From: Chad Kittel Date: Wed, 7 Feb 2024 18:21:37 +0000 Subject: [PATCH 02/12] add products --- docs/patterns/pipes-and-filters.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/patterns/pipes-and-filters.yml b/docs/patterns/pipes-and-filters.yml index 158f5ae59d..f81706e136 100644 --- a/docs/patterns/pipes-and-filters.yml +++ b/docs/patterns/pipes-and-filters.yml @@ -10,6 +10,10 @@ metadata: ms.subservice: design-pattern azureCategories: - compute +products: + - azure-blob-storage + - azure-functions + - azure-queue-storage name: Pipes and Filters pattern summary: Break down a task that performs complex processing into a series of separate elements that can be reused or reordered. thumbnailUrl: /azure/architecture/browse/thumbs/pipes-filters-solution.png From 50c28e85faf611e5c6c3aef218eb2b621bf8ec38 Mon Sep 17 00:00:00 2001 From: Chad Kittel Date: Wed, 7 Feb 2024 18:29:29 +0000 Subject: [PATCH 03/12] Acrolinx --- docs/patterns/pipes-and-filters-content.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/patterns/pipes-and-filters-content.md b/docs/patterns/pipes-and-filters-content.md index cf4c773a03..8ba66a2f47 100644 --- a/docs/patterns/pipes-and-filters-content.md +++ b/docs/patterns/pipes-and-filters-content.md @@ -72,15 +72,24 @@ This pattern might not be useful when: ## Example -You can use a sequence of message queues to provide the infrastructure that's required to implement a pipeline. An initial message queue receives unprocessed messages that becomes the start of the pipes and filters pattern implementation. A component that's implemented as a filter task listens for a message on this queue, performs its work, and then posts a new or transformed message to the next queue in the sequence. Another filter task can listen for messages on this queue, process them, post the results to another queue, and so on, until the final step that ends the pipes and filters process. This diagram illustrates a pipeline that uses message queues: +You can use a sequence of message queues to provide the infrastructure that's required to implement a pipeline. An initial message queue receives unprocessed messages that become the start of the pipes and filters pattern implementation. A component that's implemented as a filter task listens for a message on this queue, performs its work, and then posts a new or transformed message to the next queue in the sequence. Another filter task can listen for messages on this queue, process them, post the results to another queue, and so on, until the final step that ends the pipes and filters process. This diagram illustrates a pipeline that uses message queues: ![Diagram showing a pipeline that uses message queues.](./_images/pipes-and-filters-message-queues.png) -An image processing pipeline could be implemented using this pattern. If your workload takes an image, the image could pass through a series of largely independent and reorderable filters to perform actions such as content moderation, resizing, watermarking, reorientation, EXIF removal, CDN updates, and so on. In this example, the filters could be implemented as individually deployed Azure Functions or even a single Azure Function app that contains each filter as a isolated function. The use of Azure Function triggers, input bindings, and output bindings can be simplify the filter code and work automatically with a queue-based pipe using a [claim check](./claim-check.yml) to the image to process. +An image processing pipeline could be implemented using this pattern. If your workload takes an image, the image could pass through a series of largely independent and reorderable filters to perform actions such as: + +- content moderation +- resizing +- watermarking +- reorientation +- Exif metadata removal +- Content delivery network (CDN) publication + +In this example, the filters could be implemented as individually deployed Azure Functions or even a single Azure Function app that contains each filter as an isolated deployment. The use of Azure Function triggers, input bindings, and output bindings can be simplify the filter code and work automatically with a queue-based pipe using a [claim check](./claim-check.yml) to the image to process. ![Diagram showing the image processing pipeline that uses Azure Queues Storage between a series of Azure Functions to take an image from unprocessed to processed.](./_images/pipes-and-filters-image-processing-example.png) -Here is an example of what one filter, implemented as an Azure Function, triggered from a Queue Storage pipe with a claim Check to the image, and writing a new claim check to another Queue Storage pipe might look like. More code like this can be found in the [demonstration of the Pipes and Filters pattern](https://github.com/mspnp/cloud-design-patterns/tree/main/pipes-and-filters#readme) available on GitHub. +Here's an example of what one filter, implemented as an Azure Function, triggered from a Queue Storage pipe with a claim Check to the image, and writing a new claim check to another Queue Storage pipe might look like. More code like this can be found in the [demonstration of the Pipes and Filters pattern](https://github.com/mspnp/cloud-design-patterns/tree/main/pipes-and-filters#readme) available on GitHub. ```csharp // This is the "Resize" filter. It handles message from the pipe named "pipe-xfty", From c632fe9ddb6cdff114c8a0a31e63b2b3332f41f3 Mon Sep 17 00:00:00 2001 From: Chad Kittel Date: Wed, 7 Feb 2024 18:50:16 +0000 Subject: [PATCH 04/12] try svg + lightbox instead --- .../_images/pipes-and-filters-image-processing-example.svg | 3 +++ docs/patterns/pipes-and-filters-content.md | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 docs/patterns/_images/pipes-and-filters-image-processing-example.svg diff --git a/docs/patterns/_images/pipes-and-filters-image-processing-example.svg b/docs/patterns/_images/pipes-and-filters-image-processing-example.svg new file mode 100644 index 0000000000..a804982597 --- /dev/null +++ b/docs/patterns/_images/pipes-and-filters-image-processing-example.svg @@ -0,0 +1,3 @@ + + +
Azure Blob Storage
Unprocessed images
foo.png
bar.jpg
baz.gif
Storage queue pipe
(claim check to image blob)
Storage queue pipe
(claim check to image blob)
Storage queue pipe
(claim check to image blob)
Processed images
foo.png
bar.jpg
baz.gif
Icon-storage-86MsPortalFx.base.images-7foo.pngMsPortalFx.base.images-7bar.jpgMsPortalFx.base.images-7foo.pngIcon-compute-29Azure Function filter(content moderation)Icon-compute-29Azure Function filter(resize)Icon-compute-29Azure Function filter(publish to CDN)
\ No newline at end of file diff --git a/docs/patterns/pipes-and-filters-content.md b/docs/patterns/pipes-and-filters-content.md index 8ba66a2f47..5dbbb54932 100644 --- a/docs/patterns/pipes-and-filters-content.md +++ b/docs/patterns/pipes-and-filters-content.md @@ -87,7 +87,9 @@ An image processing pipeline could be implemented using this pattern. If your wo In this example, the filters could be implemented as individually deployed Azure Functions or even a single Azure Function app that contains each filter as an isolated deployment. The use of Azure Function triggers, input bindings, and output bindings can be simplify the filter code and work automatically with a queue-based pipe using a [claim check](./claim-check.yml) to the image to process. -![Diagram showing the image processing pipeline that uses Azure Queues Storage between a series of Azure Functions to take an image from unprocessed to processed.](./_images/pipes-and-filters-image-processing-example.png) +:::image type="complex" source="./_images/pipes-and-filters-image-processing-example.svg" alt-text="Diagram showing an image processing pipeline that uses Azure Queue Storage between a series of Azure Functions." lightbox="./_images/pipes-and-filters-image-processing-example.svg"::: + This diagram shows three unprocessed images on the left of various file types. To the right of those is an Azure Queue Storage pipe with claim check messages for each image; followed by an Azure Function that performs content moderation on the image as a filter. All the images are stored in an Azure Blob Storage account. There is another queue (pipe) and function (filter) that follows the first to handle image resizing. Then there is a dot dot dot which represents unshown pipes and filters. The last pipe and filter are responsible for publishing the final, fully processed image to its destination. +:::image-end::: Here's an example of what one filter, implemented as an Azure Function, triggered from a Queue Storage pipe with a claim Check to the image, and writing a new claim check to another Queue Storage pipe might look like. More code like this can be found in the [demonstration of the Pipes and Filters pattern](https://github.com/mspnp/cloud-design-patterns/tree/main/pipes-and-filters#readme) available on GitHub. From 3d0c67b6e77d1d54a0694421b7be42c3081dacab Mon Sep 17 00:00:00 2001 From: Chad Kittel Date: Wed, 7 Feb 2024 19:09:30 +0000 Subject: [PATCH 05/12] Tighten up image spacing --- .../_images/pipes-and-filters-image-processing-example.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/_images/pipes-and-filters-image-processing-example.svg b/docs/patterns/_images/pipes-and-filters-image-processing-example.svg index a804982597..80bfa4b7cf 100644 --- a/docs/patterns/_images/pipes-and-filters-image-processing-example.svg +++ b/docs/patterns/_images/pipes-and-filters-image-processing-example.svg @@ -1,3 +1,3 @@ -
Azure Blob Storage
Unprocessed images
foo.png
bar.jpg
baz.gif
Storage queue pipe
(claim check to image blob)
Storage queue pipe
(claim check to image blob)
Storage queue pipe
(claim check to image blob)
Processed images
foo.png
bar.jpg
baz.gif
Icon-storage-86MsPortalFx.base.images-7foo.pngMsPortalFx.base.images-7bar.jpgMsPortalFx.base.images-7foo.pngIcon-compute-29Azure Function filter(content moderation)Icon-compute-29Azure Function filter(resize)Icon-compute-29Azure Function filter(publish to CDN)
\ No newline at end of file +
Azure Blob Storage
Unprocessed images
foo.png
bar.jpg
baz.gif
Pipe
Azure Queue Storage
(claim check to image blob)
Pipe
Storage queue
(claim check to image blob)
Pipe
Azure Queue Storage
(claim check to image blob)
Processed images
foo.png
bar.jpg
baz.gif
Icon-storage-86MsPortalFx.base.images-7foo.pngMsPortalFx.base.images-7bar.jpgMsPortalFx.base.images-7foo.pngIcon-compute-29FilterAzure Function(content moderation)Icon-compute-29FilterAzure Function(resize)Icon-compute-29FilterAzure Function(publish)
\ No newline at end of file From 583c99dcc1ad8949e5e429396e9674111ab6745a Mon Sep 17 00:00:00 2001 From: Chad Kittel Date: Wed, 7 Feb 2024 19:14:20 +0000 Subject: [PATCH 06/12] formatting --- docs/patterns/pipes-and-filters-content.md | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/docs/patterns/pipes-and-filters-content.md b/docs/patterns/pipes-and-filters-content.md index 5dbbb54932..384c3e0dc6 100644 --- a/docs/patterns/pipes-and-filters-content.md +++ b/docs/patterns/pipes-and-filters-content.md @@ -94,27 +94,23 @@ In this example, the filters could be implemented as individually deployed Azure Here's an example of what one filter, implemented as an Azure Function, triggered from a Queue Storage pipe with a claim Check to the image, and writing a new claim check to another Queue Storage pipe might look like. More code like this can be found in the [demonstration of the Pipes and Filters pattern](https://github.com/mspnp/cloud-design-patterns/tree/main/pipes-and-filters#readme) available on GitHub. ```csharp -// This is the "Resize" filter. It handles message from the pipe named "pipe-xfty", -// performs the resize work, and places a message in the next pipe for anther filter -// to handle. +// This is the "Resize" filter. It handles claim checks from input pipe, performs the +// resize work, and places a claim check in the next pipe for anther filter to handle. [Function(nameof(ResizeFilter))] [QueueOutput("pipe-fjur", Connection = "pipe")] // Destination pipe claim check public async Task RunAsync( [QueueTrigger("pipe-xfty", Connection = "pipe")] string imageFilePath, // Source pipe claim check - [BlobInput("{QueueTrigger}", Connection = "pipe")] BlockBlobClient imageBlob, // Image to process from claim check - CancellationToken cancellationToken) + [BlobInput("{QueueTrigger}", Connection = "pipe")] BlockBlobClient imageBlob) // Image to process { _logger.LogInformation("Processing image {uri} for resizing.", imageBlob.Uri); // Idempotency checks - if (await resizeFilterWorkRequiredAsync(imageBlob)) - { - // Download image based on pipe's queue message body - - // Resize the image - - // Write resized image back to storage - } + + // Download image based on claim check in queue message body + + // Resize the image + + // Write resized image back to storage // Add location information for in next queue message and place on the next pipe _logger.LogInformation("Image resizing done or not needed. Adding image \"{filePath}\" into the next pipe.", imageFilePath); From 8c31a7d8c447cb62d28fd028e683dcd92128a6d3 Mon Sep 17 00:00:00 2001 From: Chad Kittel Date: Wed, 7 Feb 2024 19:23:59 +0000 Subject: [PATCH 07/12] add related pattern --- docs/patterns/pipes-and-filters-content.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/patterns/pipes-and-filters-content.md b/docs/patterns/pipes-and-filters-content.md index 384c3e0dc6..a43dc68102 100644 --- a/docs/patterns/pipes-and-filters-content.md +++ b/docs/patterns/pipes-and-filters-content.md @@ -112,8 +112,8 @@ public async Task RunAsync( // Write resized image back to storage - // Add location information for in next queue message and place on the next pipe - _logger.LogInformation("Image resizing done or not needed. Adding image \"{filePath}\" into the next pipe.", imageFilePath); + // Create claim check for image and place in the next pipe + _logger.LogInformation("Image resizing done or not needed. Adding image {filePath} into the next pipe.", imageFilePath); return imageFilePath; } ``` @@ -129,6 +129,7 @@ You might find the following resources helpful when you implement this pattern: The following patterns might also be relevant when you implement this pattern: +- [Claim-Check pattern](./claim-check.yml). A pipeline implemented using a queue may not hold the actual item being sent through the filters, but instead a pointer to the data that needs to be processed. The example uses a claim check in Azure Queue Storage for images stored in Azure Blob Storage. - [Competing Consumers pattern](./competing-consumers.yml). A pipeline can contain multiple instances of one or more filters. This approach is useful for running parallel instances of slow filters. It enables the system to spread the load and improve throughput. Each instance of a filter competes for input with the other instances, but two instances of a filter shouldn't be able to process the same data. This article explains the approach. - [Compute Resource Consolidation pattern](./compute-resource-consolidation.yml). It might be possible to group filters that should scale together into a single process. This article provides more information about the benefits and tradeoffs of this strategy. - [Compensating Transaction pattern](./compensating-transaction.yml). You can implement a filter as an operation that can be reversed, or that has a compensating operation that restores the state to a previous version if there's a failure. This article explains how you can implement this pattern to maintain or achieve eventual consistency. From d140e9a89575ccad6e9a1730f106d39acc39559a Mon Sep 17 00:00:00 2001 From: Chad Kittel Date: Wed, 7 Feb 2024 19:34:04 +0000 Subject: [PATCH 08/12] Tighten up image even more --- .../_images/pipes-and-filters-image-processing-example.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/_images/pipes-and-filters-image-processing-example.svg b/docs/patterns/_images/pipes-and-filters-image-processing-example.svg index 80bfa4b7cf..1704c4eee9 100644 --- a/docs/patterns/_images/pipes-and-filters-image-processing-example.svg +++ b/docs/patterns/_images/pipes-and-filters-image-processing-example.svg @@ -1,3 +1,3 @@ -
Azure Blob Storage
Unprocessed images
foo.png
bar.jpg
baz.gif
Pipe
Azure Queue Storage
(claim check to image blob)
Pipe
Storage queue
(claim check to image blob)
Pipe
Azure Queue Storage
(claim check to image blob)
Processed images
foo.png
bar.jpg
baz.gif
Icon-storage-86MsPortalFx.base.images-7foo.pngMsPortalFx.base.images-7bar.jpgMsPortalFx.base.images-7foo.pngIcon-compute-29FilterAzure Function(content moderation)Icon-compute-29FilterAzure Function(resize)Icon-compute-29FilterAzure Function(publish)
\ No newline at end of file +
Azure Blob Storage
Unprocessed images
foo.png
bar.jpg
baz.gif
Pipe
Azure Queue Storage
(claim check to image blob)
Pipe
Storage queue
(claim check to image blob)
Pipe
Azure Queue Storage
(claim check to image blob)
Processed images
foo.png
bar.jpg
baz.gif
Icon-storage-86MsPortalFx.base.images-7foo.pngMsPortalFx.base.images-7baz.gifIcon-compute-29FilterAzure Function(content moderation)Icon-compute-29FilterAzure Function(resize)Icon-compute-29FilterAzure Function(publish)MsPortalFx.base.images-7bar.jpg
\ No newline at end of file From e514bdb234493edeb3bf1f9227ffb7337f3e2f01 Mon Sep 17 00:00:00 2001 From: Chad Kittel Date: Wed, 7 Feb 2024 19:47:07 +0000 Subject: [PATCH 09/12] format image text --- .../_images/pipes-and-filters-image-processing-example.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/_images/pipes-and-filters-image-processing-example.svg b/docs/patterns/_images/pipes-and-filters-image-processing-example.svg index 1704c4eee9..fa6bb62a50 100644 --- a/docs/patterns/_images/pipes-and-filters-image-processing-example.svg +++ b/docs/patterns/_images/pipes-and-filters-image-processing-example.svg @@ -1,3 +1,3 @@ -
Azure Blob Storage
Unprocessed images
foo.png
bar.jpg
baz.gif
Pipe
Azure Queue Storage
(claim check to image blob)
Pipe
Storage queue
(claim check to image blob)
Pipe
Azure Queue Storage
(claim check to image blob)
Processed images
foo.png
bar.jpg
baz.gif
Icon-storage-86MsPortalFx.base.images-7foo.pngMsPortalFx.base.images-7baz.gifIcon-compute-29FilterAzure Function(content moderation)Icon-compute-29FilterAzure Function(resize)Icon-compute-29FilterAzure Function(publish)MsPortalFx.base.images-7bar.jpg
\ No newline at end of file +
Azure Blob Storage
Unprocessed images
foo.png
bar.jpg
baz.gif
pipe
Azure Queue Storage
(claim check to blob)
pipe
Storage queue
(claim check to blob)
pipe
Azure Queue Storage
(claim check to blob)
Processed images
foo.png
bar.jpg
baz.gif
Icon-storage-86MsPortalFx.base.images-7foo.pngMsPortalFx.base.images-7baz.gifIcon-compute-29
filter
Azure Function
(content moderation)
Icon-compute-29
filter
Azure Function
(resize)
Icon-compute-29
filter
Azure Function
(publish)
MsPortalFx.base.images-7bar.jpg
\ No newline at end of file From 858e9cbf15e3c5381db3ee7e63cdad8dcf61b63d Mon Sep 17 00:00:00 2001 From: Chad Kittel Date: Wed, 7 Feb 2024 20:02:29 +0000 Subject: [PATCH 10/12] Remove unused image --- ...pes-and-filters-image-processing-example.png | Bin 36527 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 docs/patterns/_images/pipes-and-filters-image-processing-example.png diff --git a/docs/patterns/_images/pipes-and-filters-image-processing-example.png b/docs/patterns/_images/pipes-and-filters-image-processing-example.png deleted file mode 100644 index a254c76666145a9131b25731de6a121f454b25e8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36527 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
1Qngf7nz4Xb;wUi&WynBRLq%(4V6g& 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 From 4a9cbd9d3d10c28923bf455efb23f7d57c295a2e Mon Sep 17 00:00:00 2001 From: Chad Kittel Date: Fri, 9 Feb 2024 15:26:46 -0600 Subject: [PATCH 11/12] Apply suggestions from code review Co-authored-by: Rick Hallihan <1796255+hallihan@users.noreply.github.com> --- docs/patterns/pipes-and-filters-content.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/patterns/pipes-and-filters-content.md b/docs/patterns/pipes-and-filters-content.md index a43dc68102..654d03e2e0 100644 --- a/docs/patterns/pipes-and-filters-content.md +++ b/docs/patterns/pipes-and-filters-content.md @@ -88,10 +88,10 @@ An image processing pipeline could be implemented using this pattern. If your wo In this example, the filters could be implemented as individually deployed Azure Functions or even a single Azure Function app that contains each filter as an isolated deployment. The use of Azure Function triggers, input bindings, and output bindings can be simplify the filter code and work automatically with a queue-based pipe using a [claim check](./claim-check.yml) to the image to process. :::image type="complex" source="./_images/pipes-and-filters-image-processing-example.svg" alt-text="Diagram showing an image processing pipeline that uses Azure Queue Storage between a series of Azure Functions." lightbox="./_images/pipes-and-filters-image-processing-example.svg"::: - This diagram shows three unprocessed images on the left of various file types. To the right of those is an Azure Queue Storage pipe with claim check messages for each image; followed by an Azure Function that performs content moderation on the image as a filter. All the images are stored in an Azure Blob Storage account. There is another queue (pipe) and function (filter) that follows the first to handle image resizing. Then there is a dot dot dot which represents unshown pipes and filters. The last pipe and filter are responsible for publishing the final, fully processed image to its destination. + This diagram shows three unprocessed images on the left of various file types. To the right of those is an Azure Queue Storage pipe with claim check messages for each image; followed by an Azure Function that performs content moderation on the image as a filter. All the images are stored in an Azure Blob Storage account. There is another queue (pipe) and function (filter) that follows the first to handle image resizing. Then there is an ellipses (…) which represents unshown pipes and filters. The last pipe and filter are responsible for publishing the final, fully processed image to its destination. :::image-end::: -Here's an example of what one filter, implemented as an Azure Function, triggered from a Queue Storage pipe with a claim Check to the image, and writing a new claim check to another Queue Storage pipe might look like. More code like this can be found in the [demonstration of the Pipes and Filters pattern](https://github.com/mspnp/cloud-design-patterns/tree/main/pipes-and-filters#readme) available on GitHub. +Here's an example of what one filter, implemented as an Azure Function, triggered from a Queue Storage pipe with a claim Check to the image, and writing a new claim check to another Queue Storage pipe might look like. The implementation has been replaced with pseudocode in comments for brevity. More code like this can be found in the [demonstration of the Pipes and Filters pattern](https://github.com/mspnp/cloud-design-patterns/tree/main/pipes-and-filters#readme) available on GitHub. ```csharp // This is the "Resize" filter. It handles claim checks from input pipe, performs the @@ -105,14 +105,20 @@ public async Task RunAsync( _logger.LogInformation("Processing image {uri} for resizing.", imageBlob.Uri); // Idempotency checks + // ... // Download image based on claim check in queue message body + // ... // Resize the image + // ... // Write resized image back to storage + // ... // Create claim check for image and place in the next pipe + // ... + _logger.LogInformation("Image resizing done or not needed. Adding image {filePath} into the next pipe.", imageFilePath); return imageFilePath; } From b60642cccba862f7e54e01fab473f0e52465bce8 Mon Sep 17 00:00:00 2001 From: Jeff Borsecnik <123032460+American-Dipper@users.noreply.github.com> Date: Fri, 9 Feb 2024 14:15:35 -0800 Subject: [PATCH 12/12] Update pipes-and-filters-content.md - Acolinx typo fix --- docs/patterns/pipes-and-filters-content.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/pipes-and-filters-content.md b/docs/patterns/pipes-and-filters-content.md index 654d03e2e0..d9026c16eb 100644 --- a/docs/patterns/pipes-and-filters-content.md +++ b/docs/patterns/pipes-and-filters-content.md @@ -85,7 +85,7 @@ An image processing pipeline could be implemented using this pattern. If your wo - Exif metadata removal - Content delivery network (CDN) publication -In this example, the filters could be implemented as individually deployed Azure Functions or even a single Azure Function app that contains each filter as an isolated deployment. The use of Azure Function triggers, input bindings, and output bindings can be simplify the filter code and work automatically with a queue-based pipe using a [claim check](./claim-check.yml) to the image to process. +In this example, the filters could be implemented as individually deployed Azure Functions or even a single Azure Function app that contains each filter as an isolated deployment. The use of Azure Function triggers, input bindings, and output bindings can simplify the filter code and work automatically with a queue-based pipe using a [claim check](./claim-check.yml) to the image to process. :::image type="complex" source="./_images/pipes-and-filters-image-processing-example.svg" alt-text="Diagram showing an image processing pipeline that uses Azure Queue Storage between a series of Azure Functions." lightbox="./_images/pipes-and-filters-image-processing-example.svg"::: This diagram shows three unprocessed images on the left of various file types. To the right of those is an Azure Queue Storage pipe with claim check messages for each image; followed by an Azure Function that performs content moderation on the image as a filter. All the images are stored in an Azure Blob Storage account. There is another queue (pipe) and function (filter) that follows the first to handle image resizing. Then there is an ellipses (…) which represents unshown pipes and filters. The last pipe and filter are responsible for publishing the final, fully processed image to its destination.