How to: Create an Agent for Batch Processing
Applies to: Functional Programming
Authors: Tomas Petricek and Jon Skeet
Get this book in Print, PDF, ePub and Kindle at manning.com. Use code “MSDN37b” to save 37%.
Summary: When receiving messages, the application may put the messages into groups of some maximum size and process them in batches. This article develops an agent that implements this behavior.
This topic contains the following sections.
- Introducing Batch Processing
- Implementing an Agent for Batch Processing
- Sample Batch Processing Using an Agent
- Summary
- Additional Resources
- See Also
This article is associated with Real World Functional Programming: With Examples in F# and C# by Tomas Petricek with Jon Skeet from Manning Publications (ISBN 9781933988924, copyright Manning Publications 2009, all rights reserved). No part of these chapters may be reproduced, stored in a retrieval system, or transmitted in any form or by any means—electronic, electrostatic, mechanical, photocopying, recording, or otherwise—without the prior written permission of the publisher, except in the case of brief quotations embodied in critical articles or reviews.
Introducing Batch Processing
When writing a system that receives a large number of messages at a very high speed, it may be quite inefficient to process messages one by one, as they arrive. A better option is to receive messages in groups and then process an entire group at once. For example, when inserting data into an SQL database, multiple rows can be inserted at once using bulk insert, which is significantly more efficient than inserting individual rows.
Batch processing is a very common requirement. Instead of implementing it over and over again, the behavior can be encapsulated inside an agent and then reused. This article shows how to implement a BatchProcessingAgent that can be used to add batch processing to concurrent systems in F#. The agent takes a maximum allowed size of s batch and a maximum timeout between batches and behaves as follows:
When it receives the allowed number of messages before the specified time, it reports a new bulk, resets the timer, and starts waiting for new messages.
When the timeout elapses, the agent produces a group of messages received so far (if there are any) and starts waiting again.
The agent can be used in various ways. When the maximum number of messages is very high, the agent just groups the messages received within a specified timeout. When the timeout is large, the agent always waits until it receives a specified number of messages. In practice, it is useful to combine the two options to get reasonably sized groups but also process all messages reasonably quickly.
Figure 1. Grouping messages with the maximum group size of three and a timeout
Figure 1 demonstrates the behavior of the agent. The circles represent messages received, and the lines below denote groups. In the first case, the agent receives three messages within the specified time limit, so it creates a group of three. In the second case, only two messages are received before the timeout, so the second group consists of just two values. Next, the agent receives three messages very quickly, so it creates a new group much sooner.
Implementing an Agent for Batch Processing
The agent implementation is quite simple but it is split into two sections to make the code more readable. Complete source code can be also downloaded at the end of the article. The following section starts by looking at the declaration of the batch processing agent type. The body of the agent is added later.
Creating the Agent Type
Consider the members that the batch processing agent needs to provide. When created, the agent is given the maximum size of a batch and the maximum timeout between the batches. These two numbers will be the parameters of the agent's constructor.
The agent also needs a member for sending messages, which is called Enqueue. When a batch is generated, the agent reports it back to the caller using an event called BatchProduced. The following snippet shows the structure of the BatchProcessingAgent<'T> type:
/// Agent that groups received messages in
/// groups with a specified maximum size
type BatchProcessingAgent<'T> (batchSize, timeout) =
let batchEvent = new Event<'T[]>()
let agent : Agent<'T> = Agent.Start(fun agent ->
// NOTE: Implementation details omitted
let rec loop remainingTime messages = (...)
loop timeout [] )
/// Triggered when the agent collects a group of messages
member x.BatchProduced = batchEvent.Publish
/// Send new message to the agent
member x.Enqueue(v) = agent.Post(v)
The type follows the usual pattern for creating agent-based types. It creates a new instance of agent in the constructor. The constructor of the agent also creates a new event of the Event<'T[]> type, which is used to report batches of messages to the clients of the type. When triggered, it will carry a (non-empty) array of accumulated messages.
The agent has two public members. The Enqueue method is a simple proxy that sends messages to the agent. The BatchProduced member exposes the event that reports batches. The Publish member of the event returns a value of the IEvent<'T[]> type, representing an event that clients can register with. The next section uses the Trigger method of the event to send batches to all registered clients. This is done in the body of the agent using the rules discussed in the introduction.
Implementing the Agent Body
The body of the bulking agent can be implemented quite easily using the TryReceive method of the Agent<'T> type. The method takes a timeout and resumes the asynchronous workflow as soon as a message is received or after the specified timeout. The method returns an option value that can be processed using pattern matching. Pattern matching makes it easy to recognize all possible cases:
When the agent collects a batch of messages of maximum length
When it receives a message but has still room for more messages
When the timeout elapses but the agent has some pending messages
When the timeout elapses and the agent doesn't have any pending messages
The implementation of the agent directly encodes the four cases above. In reaction to the messages, it may trigger the event and then it continues waiting for more messages:
let rec loop remainingTime messages = async {
let start = DateTime.Now
let! msg = agent.TryReceive(timeout = max 0 remainingTime)
let elapsed = int (DateTime.Now - start).TotalMilliseconds
match msg with
| Some(msg) when List.length messages = bulkSize - 1 ->
batchEvent.Trigger(msg :: messages |> List.rev |> Array.ofList)
return! loop timeout []
| Some(msg) ->
return! loop (remainingTime - elapsed) (msg::messages)
| None when List.length messages <> 0 ->
batchEvent.Trigger(messages |> List.rev |> Array.ofList)
return! loop timeout []
| None ->
return! loop timeout [] }
The agent keeps two pieces of state when looping. The parameter remainingTime stores the time until a batch needs to be returned to the client, and messages is a list of messages collected so far. For example, when the timeout is 10 seconds and the agent receives a message after 7 seconds of waiting, the messages list will contain a single element, and remainingTime will be 3 seconds.
The TryReceive method is called with the remaining time as an argument. A negative value would mean that the method should wait forever, so the snippet ensures that the remaining time is positive. After receiving a message, the snippet handles the four cases described above. When the last message in a batch is received, the agent sends the batch to the client by calling bulkEvent.Trigger. If the batch length is still smaller, the agent subtracts the time spent while waiting from the remaining time and continues looping. A received message None signifies that the timeout has elapsed. If there are any messages, the agent sends them to the client and then continues waiting for the next batch.
This snippet completed the implementation of the agent. The next section demonstrates how to use the agent for batch processing of user interface events.
Sample Batch Processing Using an Agent
The batch processing agent can be useful in a wide range of applications. It can be used in servers or other applications that need to store data in a database or in concurrent systems that receive updates via messages (for example, updates from a social network or messages in trading). This section looks at an example based on a user interface, which is the easiest way to demonstrate the agent.
The example creates a new form that handles KeyPress messages from the system and displays the entered text with a delay. After pressing five keys, the text is updated immediately. When the user presses a smaller number of keys, the form is updated after a 5-second timeout. Behavior like this can be useful when sending requests to the server. The application needs to send updates when there are significant changes, but it shouldn't flood the server.
// Initialize application user interface
open System
open System.Drawing
open System.Windows.Forms
let frm = new Form()
let lbl = new Label(Dock = DockStyle.Fill)
frm.Controls.Add(lbl)
frm.Show()
// Create agent for bulking KeyPress events
let ag = new BatchProcessingAgent<_>(5, 5000)
frm.KeyPress.Add(fun e -> ag.Enqueue(e.KeyChar))
ag.BatchProduced
|> Event.map (fun chars -> new String(chars))
|> Event.scan (+) ""
|> Event.add (fun str -> lbl.Text <- str)
The user interface for the example is just a single form with a label for displaying the entered text. The interesting part is the last few lines of code. The snippet first creates a new BatchProcessingAgent and specifies 5 as the batch size and 5000 ms as the timeout. Then it adds a KeyPress handler that sends typed characters to the agent.
The handler for BatchProduced event uses F# functions for event processing. The Event.map function turns the original event that carries arrays of chars into an event that carries strings. The Event.scan function creates an event that concatenates strings and reports the complete text written so far. Finally, Event.add creates a handler that displays the text on the form. When running the program, the text updates either after writing 5 characters or after 5 seconds, whichever happens first.
Summary
This article looked at how to implement batch processing of incoming messages using an agent. The agent provides a member Enqueue for adding messages and a BatchProduced event, which reports groups of messages to the user of the agent. This makes the agent very easy to integrate into various types of applications, ranging from web servers and trading systems to user interfaces. The type is implemented as an agent, which means that it can be safely accessed from multiple threads.
Additional Resources
This article discussed how to implement an agent that is useful for implementing batch processing, which is a very common pattern in concurrent systems. For more information about agents and server-side programming in F#, see the following articles:
The following How to article gives another example of a reusable agent:
To download the code snippets shown in this article, go to https://code.msdn.microsoft.com/Chapter-2-Concurrent-645370c3
See Also
This article is based on Real World Functional Programming: With Examples in F# and C#. Book chapters related to the content of this article are:
Book Chapter 13: “Asynchronous and data-driven programming” explains how asynchronous workflows work and uses them to write an interactive script that downloads a large dataset from the Internet.
Book Chapter 14: “Writing parallel functional programs” explains how to use the Task Parallel Library to write data-parallel and task-based parallel programs. This approach complements agent-based parallelism in F#.
Book Chapter 16: “Developing reactive functional programs” discusses how to write reactive user interfaces using asynchronous workflows and events. This approach is related to agents but more suitable for creating user interfaces.
The following MSDN documents are related to the topic of this article:
Asynchronous Workflows (F#) explains how to use F# asynchronous workflows for writing efficient computations that do not block the execution of other work.
MailboxProcessor.TryReceive<'Msg> Method (F#) provides more information about the method that we used to implement waiting for a message with a timeout.
Control.Event Module (F#) lists various F# functions for working with events such as Event.scan and Event.map used in this example.
Importing Bulk Data by Using BULK INSERT or OPENROWSET(BULK...) shows inserting data into a database in bulks as a good example of a problem that can be elegantly solved using the bulking agent.
Previous article: How to: Create a Blocking Queue Using Agents
Next article: Developing Client-Side Applications