amarok.agent

This page provides a quick overview about the most important concepts. A more detailed documentation will be provided when the concepts and APIs have proven to be useful and have been stabilized.

For the following code snippets we need to include two namespaces.

using Amarok;
using Amarok.Agents;
The programming model is all about agents and messages. So, let's start with implementing a simple message type. Messages are implemented as types that derive directly or indirectly from the base class Message.

public class LogInfo : Message
{
  public String Text
  {
    get;
    private set;
  }

  public LogInfo(String text)
  {
    Verify.NotNull(text, "text");
    this.Text = text;
  }

  public LogInfo(String format, params Object[] arguments)
  {
    Verify.NotNull(format, "format");
    Verify.NotNull(arguments, "arguments");
    this.Text = String.Format(format, arguments);
  }
}
Now, implement an agent that accepts this specific message type and outputs the supplied text to the console each time it receives a message. Agents are implemented as types derived from either Agent or Agent<E,O>. All agent types must provide a specific constructor that accepts agent environment and agent options as parameters and route them to the base class constructor.

public sealed class ConsoleLoggerAgent : Agent
{
  public ConsoleLoggerAgent(AgentEnvironment environment, AgentOptions options)
    : base(environment, options)
  {
  }

  [MessageHandler]
  private void _HandleLogInfo(LogInfo message)
  {
    Console.WriteLine(message.Text);
  }
}
To handle a message inside an agent you implement a method and attach the MessageHandlerAttribute to it. The method must accept a single parameter, which type defines which message types will be forwarded to this handler. In this example we define a method parameter of type LogInfo, thus, the method will be called for all messages of type LogInfo or types derived from LogInfo.

Now that we have a message and agent implementation, it is time to spawn a new instance of our agent and send it a few messages. You can create new instances of agents using the SpawnAgent() methods on AgentFactory. Those methods return an IAgent reference to the newly spawned agent, which defines the most important method of all agents: Post().

var agent = AgentFactory.SpawnAgent<ConsoleLoggerAgent>(
    new AgentEnvironment(),
    AgentOptions.Empty);

for(Int32 i=0; i<100; i++)
  agent.Post(new LogInfo("Message " + i.ToString()));

Console.WriteLine("done");
The output should look like this:
Message 0
Message 1
Message 2
...
done
...
Message 98
Message 99
One important point to notice here. The post calls will complete before all messages have been processed. That's why you will see "done" before the last message text output. Post() is non-blocking and the processing of messages happens asynchronously.

Another point: The order of messages is preserved. This is because the message handler is by default not allowed to take advantage of multiple processor cores to handle incoming messages. We can change this easily by applying two other attributes to the message handler.

  [MessageHandler, ConcurrentExecution, MaximumParallelism]
  private void _HandleLogInfo(LogInfo message)
  {
    Console.WriteLine(message.Text);
  }
The MaximumParallelismAttribute defines that this specific message handler is allowed to take advantage of all available processor cores to handle incoming messages, which may result in a slightly disordered output.

Now, let's see how we can leverage the logging functionality of our existing agent. We will implement a second agent that provides the functionality to write and read files and uses our existing logging facility.

First, let's define the messages for our new file access agent. The message for writing a file is simple; we just have to supply the to-be-written data. But how do we define the message for reading data that has to return a result? Here comes the concept of operation messages into play. An operation message consists of a request and a response message. The response message can contain the expected result or in case of an error an exception object. You specify the type of the operation result as generic parameter, in our case String.

public class ReadFile : Operation<String>
{
  public String FileName
  {
    get;
    private set;
  }

  public ReadFile(String fileName)
  {
    Verify.NotEmpty(fileName, "fileName");
    this.FileName = fileName;
  }
}
You don't need to implement the response message. It is implemented internally by the base class Operation<T>.

We can implement the corresponding WriteFile message in a similar way. Since the write operation does not return any result value we use the special Empty type as operation result type. Why implementing the WriteFile message as a request/response message pair? Because operation messages are not only useful for returning results or exceptions but also for just indicating completion, for example when the write operation completed.

public class WriteFile : Operation<Empty>
{
  public String FileName
  {
    get;
    private set;
  }

  public String Text
  {
    get;
    private set;
  }

  public WriteFile(String fileName, String text)
  {
    Verify.NotEmpty(fileName, "fileName");
    this.FileName = fileName;
    this.Text = text;
  }
}
The agent implementation would look like this.

public sealed class FileAccessAgent : Agent
{
  public FileAccessAgent(AgentEnvironment environment, AgentOptions options)
    : base(environment, options)
  {
  }

  [OperationHandler]
  private Empty _HandleWriteFile(WriteFile message)
  {
    File.WriteAllText(message.FileName, message.Text);
    return Empty.Instance;
  }

  [OperationHandler]
  private String _HandleReadFile(ReadFile message)
  {
    return File.ReadAllText(message.FileName);
  }
}
To handle operation messages you have to apply the OperationHandlerAttribute and the method has to have a return type that matches the operation's result type. Returning a value will complete the operation with success, throwing an exception will complete the operation in a faulted state. Either result or exception is returned with the operation's response message. The following code snippet illustrates how to use operation messages.

var agent = AgentFactory.SpawnAgent<FileAccessAgent>(
  new AgentEnvironment());

var task = agent.PostOperation(new ReadFile("c:\test.txt"));
task.Wait();
var text = task.Result;

var tasks = new Task[100];
for(Int32 i=0; i<100; i++)
{
  tasks[i] = agent.PostOperation(new WriteFile(
    "c:\test" + i.ToString() + ".txt", 
    "CONTENT"));
}
Task.WaitAll(tasks);
We said before that we want our new file access agent to utilize our logging agent. How could we achieve that? One common way would be to supply the file access agent with a reference of the logging agent, for example we could use dependency injection to inject the reference.

The agent framework advocates a different approach. Instead of posting logging messages directly to our console logger agent we could publish (think of a kind of broadcast) logging messages from our file access agent into to the world. Some other agent (or some other agents) could handle those messages.

First, let's change our existing file access agent to publish logging messages. We add two simple lines to the existing message handler.

  [OperationHandler]
  private Empty _HandleWriteFile(WriteFile message)
  {
    File.WriteAllText(message.FileName, message.Text);
    base.Publish(new LogInfo("Written to file " + message.FileName));
    return Empty.Instance;
  }

  [OperationHandler]
  private String _HandleReadFile(ReadFile message)
  {
    base.Publish(new LogInfo("Read from file " + message.FileName));
    return File.ReadAllText(message.FileName);
  }
The method Publish() broadcasts messages into the world. Since there is no subscribing agent registered those messages will be lost.

Now, we can configure our logging agent to handle logging messages published by the file access agent. We can do this easily by using Subscribe on the file access agent.

var fileAccessAgent = AgentFactory.SpawnAgent<FileAccessAgent>(
  new AgentEnvironment());

var logAgent = AgentFactory.SpawnAgent<ConsoleLoggerAgent>(
    new AgentEnvironment());

fileAccessAgent.Subscribe(
    typeof(LogInfo),
    logAgent.GetAgentReference());
The previous code snippet shows how to connect two different agents. All messages of type LogInfo would be forwarded (posted) to the supplied log agent instance. This is very nice, because the file access agent does not need to know anything about how its logging messages are handled. We could easily replace the console logger agent by another agent, as long as it handles LogInfo messages.

But there is an even better way to connect agents: a message bus. A message bus is a simple publisher-subscriber that forwards messages to connected agents. Messages published by agents that are connected to the message bus are automatically forwarded to all other agents connected to the message bus. The following code snippet illustrates how to setup a message bus and connect agents to it.

var fileAccessAgent = AgentFactory.SpawnAgent<FileAccessAgent>(
  new AgentEnvironment());

var logAgent = AgentFactory.SpawnAgent<ConsoleLoggerAgent>(
    new AgentEnvironment());

var messageBus = new MessageBus();

messageBus.Connect(fileAccessAgent);
messageBus.Connect(logAgent);

messageBus.Publish(new ReadFile("c:\test.txt"));
That's all you have to do. The last line publishes a ReadFile message onto the message bus and kicks of a series of subsequent messages and processing steps. First, the ReadFile message is forwarded to all connected agents. Only the file access agent provides a handler for this type of message and processing happens there, which publishes a new LogInfo message. The message bus forwards this LogInfo message to all connected agents and finally the console logger agent will process the LogInfo message, writing a message to the console window.


Those simple concepts can be used to build bigger agent networks, where agents are connected via a central message bus, or you can also introduce a hierarchy of message buses. Agents are loosely coupled, communicating via messages of various types that are automatically forwarded to interested agents.

At a later time, I will provide some high-level concepts about how to structure entire applications using these simple mechanism.

Last edited Aug 17, 2012 at 7:36 PM by OlafKober, version 17

Comments

No comments yet.