Managed application singletons

x::singletonapp::managed() is a wrapper for x::singletonapp::create() that gives a higher-level implementation of an application singleton design pattern. It replaces create(), as follows:

managed() takes the following parameters:

  1. A functor that instantiates an application object, and returns a x::ref to it.

  2. A x::ref to a parameter object.

  3. The singleton's running userid, which gets passed through to the underlying create(). It defaults to the process's userid.

  4. The permissions mode for the singleton socket, which gets passed through to the underlying create(). It defaults to 0700.

  5. The sameuser flag that gets passed through to x::singletonapp::validate_peer(). It defaults to true.

The first two parameters are required, the remaining are optional, and default to the values that produce a per-userid singleton. For global, systemwide singletons, the userid parameters should be set to the singleton's starting userid (usually 0), with the socket permissions of 0755 and the sameuser flag set to false managed() has the following additional requirements:

  1. The functor parameter must return a x::ref to an object that implements three methods: run(), instance(), and stop().

  2. The constructed application object's stop() can get invoked at any time in response to a signal. The application class optionally inherits from x::stoppableObj and implements its stop() method; however it can be implemented without formally inheriting from x::stoppableObj.

  3. The application object's run() method takes two parameters, as previously described: a userid, and the singleton application invocation parameter.

  4. The application invocation parameter's prototype can be declared as x::ptr<argsObj> &. Despite the invocation parameter being a x::ptr it'll never be an null reference pointer. A native reference to a x::ptr allows it to be discarded, by assigning x::ptr<argsObj>() to it, releasing the reference on the application parameter object after it is no longer needed.

  5. The application object's run() method returns a x::ref<retObj>, the return value object.

  6. Both argsObj and retObj are reference-counted objects that implement a serialization template. Both must have default constructors.

  7. The application object's instance() method receives five parameters: the invocation instance process's userid; the invocation instance's parameter object (which can also be prototyped as x::ptr<argsObj> &); the return value object, a x::ref<retObj> that's instantiated with the default destructor; a x::singletonapp::processed; and a x::ref<x::obj> mcguffin.

The run() method gets invoked by the initial application singleton invocation. It's expected to be the main singleton thread, when it returns the return object, it gets returned as the original invocation's return object.

Additional singleton application invocation threads invoke instance(), then wait for the mcguffin to get destroyed. The return objects gets deserialized back to the singleton invocation socket.

Notes on thread concurrency with managed singletons

  • It's possible that one thread invokes instance() before another thread invokes run(). This possibilty can materialize if there are two concurrent singleton invocations. The first singleton invocation constructs the application singleton, but before it finishes the race to invoke run(), the roll of the dice gives the second invocation some CPU time, and it sprints into instance().

    There are many ways to make this a non-issue. When implementing the application object as a message dispatching thread, all instance() ends up doing is placing a message into a queue, and run() takes care of it, when its turn in the spotlight arrives.

  • It's possible that instance() gets invoked just after run() terminates. As previously described, when instance() determines that the initial thread has terminated, is about to terminate, or has commited to termination, in some application-specific manner, it should return without setting the processed flag. If possible, the mcguffin argument should remain in scope as long as possible. This results in the singleton invocation attempting to restart the singleton.

    This gets handled by a message dispatching thread-based singleton automatically. The flag object, and the mcguffin, are a part of the message that gets queued up on the message queue. When the application object gets completely destroyed, everything goes out of scope, the mcguffin gets destroyed, and unprocessed flag goes back to the singleton invocation instance, which then rewinds everything.

  • Its expected that the application object gets instantiated once, for the lifetime of the application singleton process. However, the application object should be prepared to be instantiated a second time, when really hitting a lucky roll of the dice. This can occur with the following sequence of events.

    1. Only one application singleton thread is left, the initial one that's executing run().

    2. It decides to terminate. It returns. The application object gets destroyed, but the singleton thread is not yet finished, and the singleton application instance is on its remaining life support, for just a few more cycles.

    3. Another application singleton connection gets established. The singleton's death sentence is now commuted. But, the application object has been destroyed. In this situation, another application object gets constructed.

    4. Pushing this marginal situation to the next margin: the application object (or any LIBCXX reference-counted object, for mcguffin-related purposes) is considered destroyed just before its class destructor gets invoked. It's possible that another thread instantiates another application object's instance, and invokes its constructors, while the first application object's destructor is still cleaning things up.

Example of a managed application singleton

Here's an example singleton that prints the command line parameters of its every invocation, with an argument of stop stopping the singleton:

#include <x/managedsingletonapp.H>

#include <iostream>

// An object representing the arguments to an application singleton instance
// invocation. This example just passes the raw contents of argv.

class app_argsObj : virtual public x::obj {

public:

	// They get stored in a vector proper.

	std::vector<std::string> argv;

	app_argsObj() {}
	app_argsObj(int argc, char **argv_args)
		: argv(argv_args, argv_args+argc)
	{
	}

	~app_argsObj() noexcept 
	{
	}

	// ... and serialized

	template<typename iter_type> void serialize(iter_type &iter)
	{
		iter(argv);
	}

	void dump() const
	{
		const char *sep="";

		for (auto &arg:argv)
		{
			std::cout << sep << arg;
			sep=" ";
		}
		std::cout << std::endl;
	}
};

typedef x::ref<app_argsObj> app_args;
typedef x::ptr<app_argsObj> app_argsptr;

// A return value from the application singleton instance. This example
// returns a std::string, a simple message.

class ret_argsObj : virtual public x::obj {

public:
	ret_argsObj() {}
	~ret_argsObj() noexcept {}

	std::string message;

	ret_argsObj(const std::string &messageArg) : message(messageArg) {}

	template<typename iter_type> void serialize(iter_type &iter)
	{
		iter(message);
	}
};

typedef x::ref<ret_argsObj> ret_args;

class appObj : virtual public x::obj {

public:

	std::mutex mutex;
	std::condition_variable cond;

	bool quitflag;

	appObj() : quitflag(false) {}
	~appObj() noexcept {}

	// The first singleton instance waits for another instance to be
	// called with a 'stop' argument.

	ret_args run(uid_t uid, const app_args &args)
	{
		std::cout << "Initial (uid " << uid << "): ";
		args->dump();

		std::unique_lock<std::mutex> lock(mutex);

		cond.wait(lock, [this] { return quitflag; });

		return ret_args::create("Bye!");
	}

	// Additional application singleton instance invocation.

	void instance(uid_t uid, const app_args &args,
		      const ret_args &ret,
		      const x::singletonapp::processed &flag,
		      const x::ref<x::obj> &mcguffin)
	{
		flag->processed();
		std::cout << "Additional (uid " << uid << "): ";
		args->dump();
		ret->message="Processed";
		if (args->argv.size() > 1 && args->argv[1] == "stop")
			stop();
	}

	// Another way that the application singleton gets stopped. Typically
	// by a SIGINT or SIGHUP.

	void stop()
	{
		std::unique_lock<std::mutex> lock(mutex);

		quitflag=true;
		cond.notify_all();
	}
};

typedef x::ref<appObj> app;

int main(int argc, char **argv)
{
	const char *uid=getenv("SUID");
	const char *same_env=getenv("SAME");
	bool same=!same_env || atoi(same_env);

	auto ret=x::singletonapp::managed(
					  // The application singleton factory
					  [] { return app::create(); },

					  // Argument for this instance
					  // invocation.

					  app_args::create(argc, argv),
					  (uid ? (uid_t)atoi(uid):getuid()),
					  same ? 0700:0755, same);

	std::cout << "Result (uid "
		  << ret.second << "): " << ret.first->message << std::endl;
	return 0;
}

The application object's, appObj's, run() dumps the initial invocation's arguments and waits for an additional invocation with a stop parameter, then returns a return object with a single Goodbye! message.

Each invocation's instance() invokes processed() to mark the invocation as processed, dumps the additional invocation's parameters, handling the stop parameter by invoking stop().

Note

stop() also gets invoked upon receipt of an interrupt or a termination signal.

Note

The second parameter to run() and instance() can also be declared as app_argsptr &, allowing it to be nulled, and the underlying parameter object to go out of scope and get destroyed, if it's no longer needed.

Note

The purpose of the flag parameter is to signal that the singleton instance is being destroyed. An additional refinement would be to check quitflag (while holding a lock on the mutex, of course), and return from instance() without further processing, if it's set. This situation could happen, for example, if two invocations of the singleton application occur concurrently, with the first one invocing stop(), but the second invocation reaching the singleton process before run() has a chance to wake up and terminate.

It's presumed, in this case, that the singleton's lifetime is about to end, and, as previously described, the singleton invocation tries again, immediately. In this case, the singleton should truly give up the ghost immediately, since the original invocation will keep trying to take its place, burning up the CPU.

In the above example, setting the SUID environment variable to 0 (the root user), and SAME to 0 too (false), and making the initial invocation as root, makes this a global application singleton (the additional invocation must also have the same environment variables set).

Implementing a managed application singleton as a message dispatching-based thread

The semantics of instance() easily accomodates its implementation, and the implementation of the application object overall, as message dispatching-based thread.

#include <x/managedsingletonapp.H>
#include <x/eventqueuemsgdispatcher.H>
#include <x/logger.H>

class appObj : virtual public x::eventqueuemsgdispatcherObj {

// ...

public:
	ret_args run(uid_t uid, const app_args &args)
	{
		// Do the startup thing.

		try {
			while (1)
				msgqueue->pop()->dispatch();
		} catch (const x::stopexception &e)
		{
		} catch (const x::exception &e)
		{
			LOG_ERROR(e);
			LOG_TRACE(e->backtrace);
		}
	}

#include "app.msgs.all.H"
};

void appObj::dispatch(const instance_msg &msg)
{
	msg.flag->processed();

	// ...
}

The message stylesheet file then contains:

<method name="instance">
  <comment>
    //! A command from another invocation sent to the singleton instance.
  </comment>

  <param>
    <comment>
	//! The other invocation's uid
    </comment>

    <decl>uid_t <name>uid</name></decl>
  </param>

  <param type="class">
    <comment>
	//! The invocation's arguments.
    </comment>

    <decl>app_args <name>args</name></decl>
  </param>

  <param type="class">
    <comment>
	//! The return value to the original invocation
    </comment>

    <decl>ret_args <name>ret</name></decl>
  </param>

  <param type="class">
    <comment>
	//! Invoke flag->processed() to indicate that the request was processed
    </comment>

    <decl>x::singletonapp::processed <name>flag</name></decl>
  </param>

  <param type="class">
    <comment>
	//! Request mcguffin

	//! When this object goes out of scope and gets destroyed, the
	//! "ret" and "flag" gets sent back to the original invocation.
    </comment>

    <decl>x::ref&lt;x::obj&gt; <name>mcguffin</name></decl>
  </param>

</method>

Processing this stylesheet results in an instance() implementation that sends a instance_msg message to the thread, to be dispatched.

The dispatching function, in this case, always sets the processed flag. If the dispatching run() takes a x::stopexception and returns, before dispatching this message, the processed flag never gets set, and the singleton invocation proceeds and attempts to start another singleton instance, as intended.