Project links

Presentations

Mordor

What is it?

Mordor is a high performance I/O library. One of its main goals is to provide very easy-to-use abstractions and encapsulation of difficult and complex concepts, yet still provide near absolute power in wielding them if necessary. It includes the following:

  • Cooperatively scheduled fiber engine, including synchronization primitives
  • Streaming library, for dealing with streams of data and manipulating them
  • HTTP library built on top of Fibers and Streams for a simple-to-use, yet extremely powerful HTTP client and server API
  • Supporting infrastructure, including logging, configuration, statistics gathering, and exceptions
  • Lightweight, easy-to-use unit test framework with several useful features
  • Windows, Linux, and Mac OS support (32-bit and 64-bit on all platforms)
Where should it be used?

Any software (server-side or client-side) that needs to process a lot of data. It is C++, so is probably overkill for something that could be easily handled with a Python or Ruby script, but can be used for simpler tasks because it does provide some nice abstractions that you won't see elsewhere. Server applications handling lots of connections will benefit most from the Fiber engine, by transforming an event-based paradigm into a familiar thread-based paradigm, while keeping (and in some cases improving) the performance of an event-based paradigm.

What license is it released under?

Mordor is licensed under the New BSD License, and Copyright (c) 2009, Decho Corp. See our LICENSE file for details.

How is it different?

Mordor allows you to focus on performing a logical task, instead of deciding how to make that task conform to a specific threading/event model. Just because local disk I/O will block and should be performed in a thread pool while network I/O should be performed using an event based callback design, doesn't mean you can't do them both in the same function. Mordor allows you to do just that. For example, here's a complete program to read a file from disk, and send it to a socket on the network:

Forward a file from disk to a socket
#include <iostream>

#include <mordor/socket.h>
#include <mordor/streams/file.h>
#include <mordor/streams/socket.h>
#include <mordor/streams/transfer.h>

using namespace Mordor;

int main(int argc, char **argv)
{
    if (argc != 3) {
        std::cerr << "usage: " << argv[0] << " <file> <destination>" << std::endl;
        return 1;
    }
    try {
        std::vector<Address::ptr> addresses = Address::lookup(argv[2], AF_UNSPEC, SOCK_STREAM);
        Socket::ptr socket = addresses[0]->createSocket();
        socket->connect(addresses[0]);
        Stream::ptr fileStream(new FileStream(argv[1], FileStream::OPEN, FileStream::READ));
        Stream::ptr socketStream(new SocketStream(socket));
        transferStream(fileStream, socketStream);
    } catch (...) {
        std::cerr << boost::current_exception_diagnostic_information() << std::endl;
        return 2;
    }
    return 0;
}

This program is quite simple. It checks for usage, translates the string argument into a network address, creates a socket that is compatible with that address, connects to it, opens a file (as a stream), wraps the socket in a stream, and then sends the file over the socket. If an error occurs, complete error information is printed on stdout, including the type of error, the OS level error code and description (if applicable), and a complete stacktrace of the error, including debug symbol information, if available.

Looking at this code, we can see that there is only a single thread, which is all fine and dandy if this is all we're doing. But what if instead we were sending 1000 files to 1000 different sockets and didn't want to create a thread for each one? Let's say we want one thread for communicating with the network, and four threads for reading the file off the disk. Let's do it!

Forward n files from 4 disk threads to n sockets
#include <iostream>

#include <mordor/iomanager.h>
#include <mordor/scheduler.h>
#include <mordor/socket.h>
#include <mordor/streams/file.h>
#include <mordor/streams/socket.h>
#include <mordor/streams/transfer.h>

using namespace Mordor;

static void doOne(const char *file, const char *destination, IOManager &ioManager, Scheduler &scheduler)
{
    try {
        std::vector<Address::ptr> addresses = Address::lookup(destination, AF_UNSPEC, SOCK_STREAM);
        Socket::ptr socket = addresses[0]->createSocket(ioManager);
        socket->connect(addresses[0]);
        Stream::ptr fileStream(new FileStream(file, FileStream::READ, FileStream::OPEN, &ioManager,
                &scheduler));
        Stream::ptr socketStream(new SocketStream(socket));
        transferStream(fileStream, socketStream);
    } catch (...) {
        std::cerr << boost::current_exception_diagnostic_information() << std::endl;
    }
}

int main(int argc, char **argv)
{
    if (argc % 2 != 1) {
        std::cerr << "usage: " << argv[0] << " (<file> <destination>)*" << std::endl;
        return 1;
    }
    IOManager ioManager;
    WorkerPool workerPool(4, false);
    for (int i = 1; i < argc; i += 2)
        ioManager.schedule(boost::bind(&doOne, argv[i], argv[i + 1], boost::ref(ioManager),
                boost::ref(workerPool)));
    ioManager.dispatch();

    return 0;
}

Here, we re-factored most of main into doOne, but other than that it is nearly identical. This code will transfer as many files as you pass on the command line in parallel, using 5 threads.

  • The IOManager is the object used for non-blocking network I/O, and so is passed to the Socket when it is created.
  • WorkerPool is just a generic thread pool and is passed to the FileStream so that it will automatically do its work on those threads, instead of the thread it is running on when it is called.

Something to point out here is that when the work is scheduled on the IOManager, each bit of work implicitly creates a Fiber - a lightweight, cooperatively scheduled, user-mode thread. The doOne function is executed on its own Fiber, and is allowed to switch which thread it is running on (inside of FileStream), without having to do any callbacks, virtual functions on a defined interface, or anything else. Internally, when FileStream wants to execute on the thread pool, it suspends the current Fiber, allowing other Fibers to run on this thread, and is resumed on a thread in the WorkerPool. IOManager and WorkerPool both inherit from Scheduler, which is the base functionality for cooperatively scheduling Fibers. Pretty cool, eh?

Here is a simple example of the sort of threadpool switching that the above methods do for you (but that you can do, too!):

Exceptions thrown across stack-swapped threadpool switching
#include <iostream>
#include <mordor/scheduler.h>

struct MyException {};

int main(int argc, char** argv) {
    WorkerPool poolA(1, true); // pool A has one thread, and includes the caller thread
    WorkerPool poolB(1, false); // pool B has one thread, and does not include the caller thread

    try {
        // remember where we are right now, and on scope exit returns to that context
        SchedulerSwitcher switcher;

        poolB.switchTo();
        std::cout << "hello from the thread in worker pool B!" << std::endl;

        throw MyException();
    } catch (MyException &) {
        std::cout << "caught exception in worker pool A that was thrown on worker pool B!"
                  << std::endl;
    }
}