I'm currently designing an application, and I'm confused at how to break apart my monolith application. After doing some research, service fabric actors seem to be the best solution for simple processing, but I'm confused at the approach I should take.

How my current application works in my monolith:

  1. IRC Message comes in from a user through a socket event.
  2. A processor parses the message, and locates the command (Using MEF) that should ultimately process the message. For example, if the command was '!help' it would locate the help command and send the message there. The processor also looks up user settings (For example, they can change the ! to a # in settings, and the processor would need to look for '#help' instead of '!help', and passes a massive settings object (as a context basically) to the command. I use something very similar to what JabbR does here.
  3. If a command isn't found (ex !asdf).:

    A. The channel has a default command set: the command is sent to that. (ex. !asdf isn't found, and they have help as the default command. The message is basically processed as !help asdf).

    B. The channel doesn't have a default set: the message is ignored.

  4. Message is processed through that command.

Ideally, I'd like each command to have its own service (They are separate units of work). Some thoughts on how I could design this via a microservice:

  1. Message comes in from a user and channel via a service bus topic.
  2. A processor parses the message, and fires an actor (With the actor ID being the channel ID associated with that message) which will then process the message. The processor would have to manage the list of services which are associated with each message, and this processor would need to be updated on each subsequent addition of a command service.
  3. Command Actor will manage it's state internally, which means settings will be internal to itself. An "ignore list" command, will manage it's own ignore list, and provide other processes the ability to view the ignore list. (In this case, the processor could/would use the ignore list to see if a user is ignored).
  4. Certain long running actors would become stateful services, and would manage their own settings for each channel that would access that service.

Another solution:

  1. Message comes in from a user via a service bus topic.
  2. A processor parses the message, and fires an actor (With the actor ID being the user ID associated with that message) which will then process the message. The processor would have to manage the list of services which are associated with each message, and this processor would need to be updated on each subsequent addition of a command service.
  3. Actor will call a user actor to manage and receive settings.
  4. Certain long running actors would become stateful services, and would manage their own settings for each user that would access that service.

A third solution:

  1. Message comes in from a user via a service bus topic.
  2. A processor parses the message, and puts a message onto a service bus queue. Using a SQL filter, each command would be a stateless service that could then attach on to the queue, and process messages that are associated with that command. A default command in this case would not be possible (Or I'd have to poll the list of subscribers on the queue, and get a list of rules they're all listening too, and compare each command against that list. It would require me to pull this list at a regular interval to ensure new services get added to this list.)

So to summarize, I'm having 2 issues: Where to handle settings for a user, and how to handle passing the commands off to either actors or services. How should I proceed?

  • What do the different commands do? I don't need details, but a general idea of the application. Surely, the end goal of your application is not a command processor, that is just the means to the end. What is the "end" as defined by the business? – TheCatWhisperer Apr 19 '17 at 16:45
  • 1
    It's an IRCBot @TheCatWhisperer. User types !song, raw IRC message is put onto the queue, the song command processes the message, looks up the currently playing song, and sends output to the chat service that originally put it onto the queue. – FrankerZ Apr 19 '17 at 17:42
  • I thought there were many commands, that can't be the only thing it does, is it? – TheCatWhisperer Apr 19 '17 at 18:42
  • Oh no @TheCatWhisperer. There are many different commands (song being one of them). Some other examples would be (help to display help text, poll to start a poll, giveaway to start a raffle, among many others). Some are short quick responses (Like help, which would just output a message to the chat service), and some require some async processing (like song, which requires posting an mp3 to a sound cloud to identify a song playing). – FrankerZ Apr 19 '17 at 20:42
  • 1
    I'll just leave this here: Akka.Net. – Machado Apr 20 '17 at 20:30

I think you may over complicating this in some ways.

Lets think of the chat responses as your UI, each command is its own "web page", but instead of rendering html, it is rendering IRC responses (and sometimes songs, ect).

Now let's go back and reference MVC pattern, it almost always works well for a UI, and your "UI" is not one of the exceptions in my opinion. Think of each command as having its own controller, similar how in a traditional web app, each entity would have its own controller.

Bus
 |->Dispatcher
        |-----> SongController
        |-----> HelpController
        |-----> PollController
        |-----> ect

I suspect some of the trouble you may be having is from trying to impose a relationship between a commands, where there is very little.

All the above dispatcher does, is figure out what the command is, then forward the request to the appropriate controller.

You will have a separate project for your domain models, with classes such as User. A third project will contain the classes for saving/retrieving data.

The Dispatcher can also have the the message come from a service bus if you want, though, that may be over kill depending on how you have things implemented

class MessageDispatcher
{
     public void HandleMessage()
     {
          User currentUser = UserRepository.GetUser(this.GetUsername());
          Channel channel = ChannelRepository.GetChannel(this.GetChannelId())
          string commandChar = currentUser.CommandCharSetting; //usually '!', but sometimes '#'
          string command = this.ParseCommand(commandChar);
          if (command == null) //Command not found
          { 
              command = channel.DefaultCommand;
          }

          if (command == "song")
          {
              SongController.HandleMessage(currentUser, requestData);
          }
          if (command == "help")
          {
              HelpController.HandleMessage(currentUser, requestData); //Note, the method signature here CAN be different, we don't want to try to force a relationship here, where none exists. Each controller is responsible for figuring out what to do with the command itself, it may or may not need a user, which we are only passing as an optimization
          }

Next the controller will take care of the specifics of the command, including looking up any needed data from the repositories, or handling the buses/asych stuff like starting a task to send a song to the appropriate address.

class HelpController
{
     public void HandleMessage(user, requestData)
     {
          HelpResponseModel model = new HelpResponseModel();
          model.NumberOfPeopleHelped = StatsRepo.GetTotalPeopleHelped();
          model.CommandChar = user.CommandCharSetting;
          StatsRepo.IncrementTotalPeopleHelped();

          new HelpView().Render(model);
          //The song controller would look up the song and send the file instead
     }
 }

class HelpView : IRCView
{
    public void Render(HelpModel model)
    {
        this.WriteLine("{0} have asked for help! :)", model.NumberOfPeopleHelped);
        this.WriteLine("{0}help - get help", model.CommandChar);
        this.WriteLine("{0}poll - get opinions, model.CommandChar);
        // ect
     }
}

Your song controller will be very different:

class SongController
{
     public HandleMessage(user, requestData)
     {
          int currentSongId = this.GetCurrentlyPlayingSongId()

          ExecuteNewTask(() =>
          {
               Song song = SongRepo.GetSong(currentSongId);
               FTPSend(this.ParseOutAddress(requestData), Song.AudioData);
          });
          new SongView().Render(); //<---- Current Song Sent :)
      }
}

Obviously, this exact code will not work for you, but I hope you get the idea, and don't try to over complicate things. This may not have addressed performance concerns as much as you would have liked, but this design will make it easier to measure and replace details as needed.

  • The biggest issue I'm trying to wrap my head around is the dispatcher: In your solution, the dispatcher is grabbing the user/channel and passing that data off to the appropriate commands. Every time a command gets updated or added at this point, I'd need to update the dispatcher as well. This seems against the microservice approach, as now I'm making the dispatcher have a relationship with each of the commands. What are your thoughts? – FrankerZ Apr 21 '17 at 0:47
  • "Obviously, this exact code will not work for you". If those dependencies bother you, you can make it happen via reflection, or via a lookup, ect. However, SOMETHING has to decide what command is being called; in this case that something is the dispatcher. Adding a new command is not a trivial thing, a user will not just add a new command, do not treat them like they are interchangeable data, they have no relationship with each other. Also, the dispatcher will NOT have to change if a command is modified, because it has nothing to do with the command other then deferring to it. – TheCatWhisperer Apr 21 '17 at 5:41
  • Yes, you are correct, this has has nothing to do with micro-services. You don't need a microservice, it will add a lot of extra work with no benefit. Microservices are for when two different departments can't come to an technical agreement... "I need to get data from database A to server B, but networking will only let traffic through port 80 or 443, time to build a microservice :( !" Is this the situation you are in? If not, why build a microservice? – TheCatWhisperer Apr 21 '17 at 5:47
  • The problem with treating a chat environment as request/response is that you often want multiple responses to indicate progress while something is occuring, and you sometimes want to proactively send a message, or ask the user for additional information. The message-passing actor system is a much better fit for a chatbot. – Joeri Sebrechts Apr 21 '17 at 8:25
  • Could you provide an answer with an example of what you're talking about @JoeriSebrechts? – FrankerZ Apr 21 '17 at 11:32

Your Answer

 

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.