Mercurial > projects > mde
diff mde/setup/Init.d @ 85:56c0ddd90193
Intermediate commit (not stable). Changes to init system.
author | Diggory Hardy <diggory.hardy@gmail.com> |
---|---|
date | Thu, 11 Sep 2008 11:33:51 +0100 |
parents | 3dfd934100f7 |
children | 79d816b3e2d2 |
line wrap: on
line diff
--- a/mde/setup/Init.d Sun Aug 31 15:59:17 2008 +0100 +++ b/mde/setup/Init.d Thu Sep 11 11:33:51 2008 +0100 @@ -14,30 +14,50 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. */ /************************************************************************************************** - * Initialisation setup and exit cleanup module. + * Initialisation and cleanup (shutdown) module. * - * This module provides an infrastructure for handling much of the initialisation and - * deinitialisation of the program. It does not, however, provide much of the (de)initialisation - * code; with the exception of that for the logger. + * Program startup follows this sequence: static this() functions, pre-init, init. + * Shutdown consists of: cleanup, post-cleanup, static ~this() functions. + * + * static this and ~this functions should not use any external resources, such as dynamically + * loaded libraries or files, and should not interact with other modules. They should be almost + * guaranteed not to fail. Preferably, they shouldn't involve large amounts of memory or + * processing, but this is not a requirement. + * + * Pre-init: init code written in this module. Generally only prerequisets of most other stages + * go here. + * + * Init: This is where init code from external modules gets hooked in. Each stage consists of an + * initialization function, a corresponding cleanup function, a state, and any dependencies (other + * init functions which must be run before this one). Init functions are run according to these + * dependencies, potentially threaded simultaeneously with other init functions. + * + * Cleanup: Cleanup happens similarly to init for all stages requiring shutdown (according to their + * state). The init dependencies are applied in reverse order (so if X depended on Y, Y's cleanup + * will not run until X's cleanup has completed), and again the functions may be threaded. + * + * Post-cleanup: like pre-init, this is code written in Init. *************************************************************************************************/ module mde.setup.Init; -import mde.setup.init2; // This module is responsible for setting up some init functions. -import mde.setup.initFunctions; +import mde.setup.InitStage; // Controls external delegates run by init import mde.setup.exception; import mde.lookup.Options; import paths = mde.setup.paths; -import mde.exception; +import mde.exception; // optionsLoadException // tango imports import tango.core.Thread; +import tango.core.sync.Condition; import tango.core.Exception; -import tango.stdc.stringz : fromStringz; +import tango.util.container.LinkedList; + +//import tango.stdc.stringz : fromStringz; import tango.io.Console; // for printing command-line usage import TimeStamp = tango.text.convert.TimeStamp, tango.time.WallClock; // output date in log file import tango.util.Arguments; -import tango.util.log.Log : Log, Logger, Level; +import tango.util.log.Log; import tango.util.log.AppendConsole; import tango.util.log.AppendFiles; @@ -48,60 +68,32 @@ import derelict.freetype.ft; import derelict.util.exception; -/** - * Static CTOR - * - * This should handle a minimal amount of functionality where useful. For instance, configuring the - * logger here and not in Init allows unittests to use the logger. - */ -static this() -{ - Logger root = Log.root; - // Set the level here, but set it again once options have been loaded: - debug root.level(Logger.Trace); - else root.level(Logger.Info); - // Temporarily log to the console (until we've found paths and loaded options): - root.add(new AppendConsole); -} -static ~this() -{ -} -/** +/************************************************************************************************** * Init class * * A scope class created at beginning of the program and destroyed at the end; thus the CTOR * handles program initialisation and the DTOR handles program cleanup. - */ + *************************************************************************************************/ scope class Init { - //private bool failure = false; // set true on failure during init, so that clean - private static Logger logger; static this() { + // Set up the logger temporarily (until pre-init): + Logger root = Log.root; + debug root.level(Logger.Trace); + else root.level(Logger.Info); + root.add(new AppendConsole); + logger = Log.getLogger ("mde.setup.Init"); } - /** this() − initialisation - * - * Runs general initialisation code, in a threaded manner where this isn't difficult. - * If any init fails, cleanup is still handled by ~this(). - * - * Init order: 1. Pre-init (loads components needed by most init functions). 2. Dynamic library - * loading (load any dynamic libraries first, so that if loading fails nothing else need be - * done). 3. Init functions (threaded functions handling the rest of initialisation). - */ - /* In a single-threaded function this could be done with: - * scope(failure) cleanup; - * This won't work with a threaded init function since any threads completing succesfully will - * not clean-up, and a fixed list of clean-up functions cannot be used since clean-up functions - * must not run where the initialisation functions have failed. - * Hence a list of clean-up functions is built similarly to scope(failure) --- see addCleanupFct. - */ + /** this() − pre-init and init */ this(char[][] cmdArgs) { - debug logger.trace ("Init: starting"); - - //BEGIN Pre-init (stage init0) + /****************************************************************************************** + * Pre-init - init code written in this module. + *****************************************************************************************/ + debug logger.trace ("Init: starting pre-init"); //FIXME: warn on invalid arguments, including base-path on non-Windows // But Arguments doesn't support this (in tango 0.99.6 and in r3563). Arguments args; @@ -210,23 +202,21 @@ } debug logger.trace ("Init: dynamic libraries loaded"); //END Load dynamic libraries - //END Pre-init - //BEGIN Init (stages init2, init4) - /* Call init functions. - * - * Current method is to try using threads, and on failure assume no threads were actually - * created and run functions in a non-threaded manner. */ + /****************************************************************************************** + * Init − where init code from external modules gets hooked in. + *****************************************************************************************/ + debug logger.trace ("Init: done pre-init, starting init stages"); - try { - if (runStageThreaded (init)) runStageForward (init); - } - catch (InitStageException) { // This init stage failed. - // FIXME: check DTOR still runs - throw new InitException ("An init function failed (see above message(s))"); - } - //END Init + // Calculate reverse dependencies of stages: + foreach (key,stage_p; stages) + foreach (name; stage_p.depends) + stages[name].rdepends ~= key; + if (miscOpts.numThreads < 1 || miscOpts.numThreads > 64) // limit to a sensible number of threads + miscOpts.set!(int)("numThreads", 4); // FIXME enforce limit in Options + + runStages!(true); // startup delegates debug logger.trace ("Init: done"); } @@ -236,113 +226,153 @@ { debug logger.trace ("Cleanup: starting"); - Options.save(); // save options... do so here for now + runStages!(false); // cleanup delegates - // General cleanup: - try { - if (runStageThreaded (cleanup)) runStageReverse (cleanup); - } - catch (InitStageException) { - // Nothing else to do but report: - logger.error ("One or more cleanup functions failed!"); - } + Options.save(); // save options... do so here for now debug logger.trace ("Cleanup: done"); } - - //BEGIN runStage... - private static { - /* The following three functions, runStage*, each run all functions in a stage in some order, - * catching any exceptions thrown by the functions (although this isn't guaranteed for threads), - * and throw an InitStageException on initFailure. */ - - const LOG_IF_MSG = "Init function "; - const LOG_CF_MSG = "Cleanup function "; - const LOG_F_START = " - running"; - const LOG_F_END = " - completed"; - const LOG_F_BAD = " - failed"; - const LOG_F_FAIL = " - exception: "; - /* Runs all functions consecutively, first-to-last. - * If any function fails, halts immediately. */ - void runStageForward (InitStage s) { - foreach (func; s.funcs) { - if (initFailure) break; - try { - debug logger.trace (LOG_IF_MSG ~ func.name ~ LOG_F_START); - func.func(); - debug logger.trace (LOG_IF_MSG ~ func.name ~ (initFailure ? LOG_F_BAD : LOG_F_END)); - } catch (Exception e) { - logger.fatal (LOG_IF_MSG ~ func.name ~ LOG_F_FAIL ~ - ((e.msg is null || e.msg == "") ? "(no failure message)" : e.msg) ); - - setInitFailure(); + // run init stages or cleanup if startup is false + private static void runStages(bool startup) () { + LinkedList!(InitStage*) toRun = new LinkedList!(InitStage*); + foreach (v; stages) { + // Filter only stages with the relevant delegate. It is not checked later that the + // delegate exists! + // If no init/cleanup function exists, implicit activation/deactivation can occur so + // that dependents/dependencies may activate/deactivate. + static if (startup) { + if (v.state == StageState.INACTIVE) { + if ((*v).init) + toRun.append (v); + else + v.state = StageState.ACTIVE; } - } - - if (initFailure) throw new InitStageException; // Problem running; abort and cleanup from here. - } - /* Runs all functions consecutively, last-to-first. - * If any function fails, continue until all have been run. */ - void runStageReverse (InitStage s) { - foreach_reverse (func; s.funcs) { - try { - debug logger.trace (LOG_CF_MSG ~ func.name ~ LOG_F_START); - func.func(); - debug logger.trace (LOG_CF_MSG ~ func.name ~ (initFailure ? LOG_F_BAD : LOG_F_END)); - } catch (Exception e) { - logger.fatal (LOG_CF_MSG ~ func.name ~ LOG_F_FAIL ~ - ((e.msg is null || e.msg == "") ? "(no failure message)" : e.msg) ); - - setInitFailure(); + } else { + if (v.state == StageState.ACTIVE) { + if (v.cleanup) + toRun.append (v); + else + v.state = StageState.INACTIVE; } } - if (initFailure) throw new InitStageException; // Problem running; abort and cleanup from here. } - /* Tries running functions in a threaded way. Returns false if successful, true if not but - * functions should be run without threads. */ - bool runStageThreaded (InitStage s) { - if (!miscOpts.useThreads) return true; // Use unthreaded route instead + debug logger.trace ("Added {} init/cleanup stages", toRun.size); + int numWorking = miscOpts.numThreads; + bool abortInit = false; + Mutex toRunM = new Mutex; // synchronization on toRun, numWorking + Condition toRunC = new Condition(toRunM); // used by threads waiting for remaining stages' dependencies to be met - ThreadGroup tg; - try { // creating/starting threads could fail - tg = new ThreadGroup; - foreach (func; s.funcs) { // Start all threads - debug logger.trace (LOG_IF_MSG ~ func.name ~ LOG_F_START); - tg.create(func.func); - debug logger.trace (LOG_IF_MSG ~ func.name ~ (initFailure ? LOG_F_BAD : LOG_F_END)); - } - } catch (ThreadException e) { // Problem with threading; try without threads - logger.error ("Caught ThreadException while trying to create threads:"); - logger.error (e.msg); - logger.info ("Will disable threads and continue, assuming no threads were created."); + /* This is a threadable member function to run init stages. + * Operation follows: + * 1 Look for a stage to run: + * if found: + * notify waiting threads work may be available + * init stage + * goto 1 + * if not found: + * if any other threads are working, wait (dependencies may not be met yet) + * if no other threads are working, notify waiting threads and terminate (finished) + * When notified, threads start at 1. */ + void initThreadFct () { + debug logger.trace ("initThreadFct: starting"); + try { // created as a thread - must not throw exceptions + InitStage* stage; - miscOpts.set!(bool)("useThreads", false); // Disable threads entirely - return true; // Try again without threads - } - - /* Wait for all threads to complete. - * - * If something went wrong, we still need to do this before cleaning up. - */ - foreach (t; tg) { - try { - t.join (true); - } catch (Exception e) { - // Relying on catching exceptions thrown by other threads is a bad idea. - // Hence all threads should catch their own exceptions and return safely. + threadLoop: while (true) { // thread loops until a problem occurs or nothing else can be done + // Look for a job: + synchronized (toRunM) { + --numWorking; // stopped working: looking/waiting for a job + if (abortInit) break threadLoop; // something went wrong in another thread + static if (startup) + int num_rdepends = (stage is null) ? 0 : stage.rdepends.length; + else + int num_rdepends = (stage is null) ? 0 : stage.depends.length; - logger.fatal ("Unhandled exception from Init function:"); - logger.fatal (e.msg); - - setInitFailure (); // abort (but join other threads first) + getStage: while (true) { + auto it = toRun.iterator; // asserts if toRun is empty + itStages: while (it.next (stage)) { // get next element of toRun + static if (startup) { + foreach (d; stage.depends) + if (stages[d].state != StageState.ACTIVE) + continue itStages; // dependency isn't met (yet) + } else { + foreach (d; stage.rdepends) + if (stages[d].state != StageState.INACTIVE) + continue itStages; // reverse dependency isn't unmet (yet) + } + + // All dependencies met + it.remove; + break getStage; + } + + // No stage remaining with all dependencies met + if (!toRun.isEmpty && numWorking) // still some working so more dependencies may be met later + toRunC.wait; // wait until another thread finishes + else // no thread is working, so none of what's left is doable, or nothing's left + break threadLoop; } + ++numWorking; // got a job! + if (num_rdepends > 2) // how many stages depended on the last one run? + toRunC.notifyAll(); // tell all waiting threads there may be work now + else if (num_rdepends == 2) + toRunC.notify(); // there's potentially work for this thread and one other + // else there won't be additional work so don't notify } - if (initFailure) throw new InitStageException; // Problem running; abort and cleanup from here. - return false; // Done successfully + // Do a job: + try { + // FIXME - old stage start&finish trace messages - we don't have a name! + debug logger.trace ("InitStage {}: starting", stage); + static if (startup) + stage.state = (*stage).init(); // init is a property of a pointer (oh no!) + else + stage.state = stage.cleanup(); + debug logger.trace ("InitStage {}: completed; state: {}", stage, stage.state); + } catch (InitStageException e) { + debug logger.trace ("InitStage {}: failed", stage); + stage.state = e.state; + abortInit = true; + break threadLoop; + } catch (Exception e) { + debug logger.trace ("InitStage {}: failed", stage); + abortInit = true; + break threadLoop; + } + } + } catch (Exception e) { + logger.fatal ("Exception in initThreadFct: "~e.msg); + abortInit = true; + } + toRunC.notifyAll(); // Most likely if we're exiting, we should make sure others aren't waiting. + debug logger.trace ("initThreadFct: returning"); + return; } - //END runStage... + + // Start miscOpts.NumThreads - 1 threads: + try { + ThreadGroup g = new ThreadGroup; + for (int i = miscOpts.numThreads; i > 1; --i) + g.create (&initThreadFct); + initThreadFct(); // also run in current thread + g.joinAll (false); // don't rethrow exceptions - there SHOULD NOT be any + } catch (ThreadException e) { + logger.error ("Exception while using threads: "~e.msg); + logger.error ("Disabling threads and attempting to continue."); + miscOpts.set!(int)("NumThreads", 1); // count includes current thread + initThreadFct(); // try with just this thread + } // any other exception will be caught in main() and abort program + + if (abortInit) + throw new InitException ("An init/cleanup function failed (see above message(s))"); + + foreach (stage; toRun) + logger.warn ("InitStage {}: was not run due to unmet dependencies", stage); + } + + private static { + Logger logger; void printUsage (char[] progName) { Cout ("mde [no version]").newline; @@ -366,32 +396,90 @@ } debug (mdeUnitTest) unittest { - /* Fake init and cleanup. Use unittest-specific init and cleanup InitStages to avoid - * messing other init/cleanup up. */ - static InitStage initUT, cleanupUT; + logger.trace ("Starting unittest"); + + auto realInit = stages; // backup the real init stages + stages = new typeof(stages); // an empty test-bed + + bool init1, init2, init3 = true; + bool failed = false; // set if anything goes wrong + StageState s1InitReturns = StageState.ACTIVE; + addInitStage ("stg1", delegate StageState() { + init1 = true; + return s1InitReturns; + }, delegate StageState() { + init1 = false; + return StageState.INACTIVE; + }); + addInitStage ("stg2", delegate StageState() { + if (!init1) failed = true; + init2 = true; + return StageState.ACTIVE; + }, delegate StageState() { + if (!init1) failed = true; + init2 = false; + return StageState.INACTIVE; + }, ["stg1"]); + InitStage s3; + s3.init = delegate StageState() { + throw new InitStageException (cast(StageState)7); // not a normal state, but good for a test + return StageState.ERROR; + }; + s3.cleanup = delegate StageState() { + if (!init1) failed = true; + init3 = false; + return StageState.INACTIVE; + }; + s3.depends = [ toStageName("stg1") ]; + s3.state = StageState.ACTIVE; // already active, so s3.init should not run (first time) + addInitStage ("stg3", &s3); - static bool initialised = false; - static void cleanupFunc1 () { - initialised = false; - } - static void cleanupFunc2 () { - assert (initialised == true); - } - - static void initFunc () { - initialised = true; - cleanupUT.addFunc (&cleanupFunc1, "UT cleanup 1"); - cleanupUT.addFunc (&cleanupFunc2, "UT cleanup 2"); - } + // Stuff normally done in Init.this(): + // Calculate reverse dependencies of stages: + foreach (key,stage_p; stages) + foreach (name; stage_p.depends) + stages[name].rdepends ~= key; + if (miscOpts.numThreads < 1 || miscOpts.numThreads > 64) // limit to a sensible number of threads + miscOpts.set!(int)("numThreads", 4); // FIXME enforce limit in Options + + + // Run the above. + runStages!(true); + assert (init1); + assert (init2); + assert (!failed); + foreach (s; stages) + assert (s.state == StageState.ACTIVE); - initUT.addFunc (&initFunc, "UT init"); + runStages!(false); + assert (!init1); + assert (!init2); + assert (!init3); + assert (!failed); + foreach (s; stages) + assert (s.state == StageState.INACTIVE); - runStageForward (initUT); - assert (initialised); + s1InitReturns = StageState.ERROR; + // Run again. S2/S3 shouldn't run, S1 won't shut down + runStages!(true); + assert (init1); + assert (!init2); + assert (!init3); + assert (!failed); + runStages!(false); + assert (init1); // S1 cleanup won't run - runStageReverse (cleanupUT); - assert (!initialised); - + stages[toStageName("stg1")].state = StageState.INACTIVE; // hack it back so we can still test + s1InitReturns = StageState.ACTIVE; + init1 = false; + try { + runStages!(true); + assert (false); // runStages should throw because s3.init runs now + } catch (Exception) {} + assert (init1); // s1.init should run first + assert (stages[toStageName("stg3")].state == cast(StageState)7); // set by the exception + + stages = realInit; // restore the real init stages logger.info ("Unittest complete."); } }