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
() supplies the
thread factory for create
().
Instead of a thread factory, the first parameter to
managed
() is a functor. The functor instantiates
an application object.
managed
() handles the singleton instance returned
by create
(), and the singleton socket connection.
Each invocation of an application singleton's instance constructs a
“parameter object”.
The parameter object represents the parameters
of this particular singleton invocation.
They parameter object typically contains
main
()'s argc/argv, maybe something from
the environment, or some other input source.
Similarly, the singleton instance produces a
“return value object”,
which gets returned by managed
(), that
represents the return value from the singleton instance.
Naturally, the initial invocation of the application singleton does
not return until the entire singleton process stops.
For subsequent invocation of the application singleton, the
application controls whether it gives the return value object
immediately, back to the additional singleton invocation, or at
some point later, perhaps when the application singleton itself
stops, too. The additional application singleton invocation
waits patiently for its return object.
managed
()
serializes the parameter object
into the socket connection to the singleton instance.
managed
()
implements a thread factory, for create
() that:
Deserializes the parameter object.
If this is the initial application singleton thread, invokes
the factory functor to instantiate an application object, then
invokes the instantiated object's
run
() method, giving it the
singleton invocation instance's userid (which, in nearly all cases
will be the same userid that invoked the application singleton),
and the invocation's parameter object.
run
() is expected to return
the return value.
For additional application singleton thread, the
previously-instantiated application object's
instance
() object gets invoked. Its
parameters are:
the additional singleton invocation instance's userid,
(which may or may not be a different userid, depending upon
whether
the application singleton is a systemwide application singleton);
the invocation parameter object; a constructed
return value object;
a constructed processed flag object; and a mcguffin. After
instance
() returns, the thread waits
for the mcgufin to go out of scope, and get destroyed.
Serializes the return object, and the processed flag (which is always presumed to be true in the case of an initial singleton invocation) to the singleton connection socket.
managed
() deserializes the return object from
the socket connection, and the processed flag; if it's
true, managed
() returns a
std::pair
containing the return object,
and the singleton process's userid.
Otherwise the parameter object is deemed as not processed, and
managed
() repeats everything again.
managed
()
takes the following
parameters:
A functor that instantiates an application object, and returns
a x::ref
to it.
A x::ref
to a parameter object.
The singleton's running userid, which gets passed through to
the underlying create
(). It defaults to the
process's userid.
The permissions mode for the singleton socket,
which gets passed through to
the underlying create
(). It defaults to
0700
.
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:
The functor parameter must return a
x::ref
to an object that implements three
methods: run
(),
instance
(), and
stop
().
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
.
The application object's run
() method
takes two parameters, as previously described: a userid, and the
singleton application invocation parameter.
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.
The application object's run
() method
returns a
“x::ref<retObj
>”,
the return value object.
Both argsObj
and
retObj
are reference-counted objects that
implement a serialization
template. Both must have default constructors.
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.
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.
Only one application singleton thread is left, the initial one
that's executing run
().
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.
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.
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.
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() { } // ... and serialized template<typename ptr_type, typename iter_type> static void serialize(ptr_type ptr, iter_type &iter) { iter(ptr->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() {} std::string message; ret_argsObj(const std::string &messageArg) : message(messageArg) {} template<typename ptr_type, typename iter_type> static void serialize(ptr_type ptr, iter_type &iter) { iter(ptr->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() {} // 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().
stop
() also gets invoked upon
receipt of
an interrupt or a termination signal.
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.
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).
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/msgdispatcher.H> #include <x/logger.H> class appObj : virtual public x::threadmsgdispatcherObj { // ... public: ret_args run(x::ptr<x::obj> &threadmsgdispatcher_mcguffin, uid_t uid, const app_args &args) { msgqueue_auto msgqueue(this); threadmsgdispatcher_mcguffin=nullptr; try { while (1) msgqueue.event(); } catch (const x::stopexception &e) { } catch (const x::exception &e) { LOG_ERROR(e); LOG_TRACE(e->backtrace); } } #include "app.msgs.H" }; void appObj::dispatch_instance(uid_t uid, const test1argsptr &args, const test1args &ret, const x::singletonapp::processed &flag, const x::ref<LIBCXX_NAMESPACE::obj> &mcguffin) { 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 uid</decl> </param> <param> <comment> //! The invocation's arguments. </comment> <decl>const app_args &args</decl> </param> <param> <comment> //! The return value to the original invocation </comment> <decl>const ret_args &ret</decl> </param> <param> <comment> //! Invoke flag->processed() to indicate that the request was processed </comment> <decl>const x::singletonapp::processed &flag</decl> </param> <param> <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>const x::ref<x::obj> &mcguffin</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.