Chapter 26. Application singleton design pattern

Index

Singleton startup and termination
Signals in singleton processes
Peer validation
Systemwide application singletons
Managed application singletons
Notes on thread concurrency with managed singletons
Example of a managed application singleton
Implementing a managed application singleton as a message dispatching-based thread

An application singleton design pattern has a single process running an application. An attempt to start another process running the same application results in the first process taking over and performing some appropriate action, like opening a new window or a file, with the second process terminating and not taking any action. With an application singleton design pattern, a single process executes the action defined by running another application process.

#include <x/singletonapp.H>

class singletonThreadObj : virtual public x::obj {

public:
    singletonThreadObj();
    ~singletonThreadObj() noexcept;

    void run(const x::fd &connection)
    {
        x::singletonapp::validate_peer(connection);

        // ...
    }
};

typedef x::ref<singletonThreadObj> singletonThread;

class threadFactoryObj : virtual public x::obj {

public:
    threadFactoryObj();
    ~threadFactoryObj() noexcept;

    singletonThread newThread()
    {
        return singletonThread::create();
    }
};

int main()
{
    auto factory=x::ref<threadFactoryObj>::create();

    x::singletonapp::instance instance=x::singletonapp::create(factory);

    x::fd connection=instance->connection;

    x::singletonapp::validate_peer(connection);

    return 0;
}

x::singletonapp implements an application singleton design pattern by, essentially, establishing socket connections from multiple processes that run the same application to individual threads within a single process, which forms the singleton.

The argument to x::singletonapp::create() is your thread factory. Your thread factory implements the newThread() method. It returns a reference to an object with a run() that takes a file descriptor as an argument.

Your newThread() does not run a new thread, only constructs a new object. Each call to newThread() must construct a new object. newThread() must not return the same object for each thread. Furthermore, no other references to the constructed object must remain in scope. After the thread object's run() returns, it's expected that no other references to the object exist and the object gets destroyed.

An application singleton process constructs the factory object, and calls x::singletonapp::create(). This function returns an x::singletonapp::instance with a connection member which is a file descriptor of an open socket.

If this is the first application process, x::singletonapp::create() calls newThread(), followed by x::run to start a new thread running the thread object's run(), with a socket that's connected to the connection in the x::singletonapp::instance. If there's already an existing process running this application, the factory object that's created in the new process is not used. The existing process's factory object invokes newThread(), and the new thread that executes the resulting object's run() gets a socket that's connected to the socket in the other process's connection.

Singleton startup and termination

The scope of x::singletonapp is limited to establishing socket connections from multiple processes running the same application to multiple threads running in a first process that's running the application. Once the sockets are established, x::singletonapp's job is done. It's expected that a singleton process's main() uses the socket to send the application startup parameters, commands, options, and similar information to its peer thread, and then terminate immediately (or, have managed() take care of it, as described later). If this is the first application process, that started the first thread, x::singletonapp::instance's destructor waits until all threads have stopped. If this is not the first application process, there are no threads to wait for, and the instance object gets destroyed immediately, and main() returns.

If, before the first thread terminated, another process resulted in the first application process starting a new singleton thread, the x::singletonapp::instance's destructor in the first process keeps waiting until all threads stop, then finishes destroying the instance object, the first process finally terminates, and the singleton is now permanently stopped.

The previous example has a short main() that instantiates a x::singletonapp::instance reference. Presumably the ommited part of main() sends startup parameters to the first singleton threads, then exits the scope. At this point, the destructor's instance waits for all singleton threads to stop. Any new threads, started as a result of connections from other singleton processes, will also have to stop, before the destructor in the first process completes.

In other application processes, there are no threads to stop, and the destructor completes immediately (and the factory object does not get used, in those processes).