server/state: Check that modules are accessed according to dependencies to avoid deadlocks

This commit is contained in:
Perttu Ahola 2014-10-27 16:55:08 +02:00
parent 35700e7082
commit 3501693188
4 changed files with 85 additions and 13 deletions

View File

@ -37,6 +37,11 @@ Function naming:
Do not use assert(); throw anonymous exceptions instead: Do not use assert(); throw anonymous exceptions instead:
- if(fail) throw Exception("Thing failed"); - if(fail) throw Exception("Thing failed");
In normal operation, zero exceptions should be thrown. Unless any potential bugs
are occurring, you should be able to play the game fine if you set a breakpoint
on a GCC Linux build to __cxa_throw, which will stop the program immediately
when an exception occurs.
Naming: Naming:
- "type": Numeric id representing a type - "type": Numeric id representing a type
- "name": A string; generally works as an identifier but not necessarily - "name": A string; generally works as an identifier but not necessarily

View File

@ -68,7 +68,5 @@ Buildat TODO
- Make some kind of an ace of spades clone (or less of a clone); something that - Make some kind of an ace of spades clone (or less of a clone); something that
requires the game to control the creation and deletion of the world anyway, requires the game to control the creation and deletion of the world anyway,
and implement the required interfaces and implement the required interfaces
- Check that modules are accessed in order of dependency to effectively maintain
recursive locking
- Fix everything in /games/ - Fix everything in /games/
- Use builtin/main_context - Use builtin/main_context

View File

@ -65,8 +65,10 @@ namespace interface
virtual ss_ get_module_path(const ss_ &module_name) = 0; virtual ss_ get_module_path(const ss_ &module_name) = 0;
virtual bool has_module(const ss_ &module_name) = 0; virtual bool has_module(const ss_ &module_name) = 0;
virtual sv_<ss_> get_loaded_modules() = 0; virtual sv_<ss_> get_loaded_modules() = 0;
virtual bool access_module(const ss_ &module_name, virtual bool access_module(const ss_ &module_name, // Always returns true
std::function<void(interface::Module*)> cb) = 0; std::function<void(interface::Module*)> cb) = 0;
/*virtual bool access_module_optional(const ss_ &module_name, TODO
std::function<void(interface::Module*)> cb) = 0;*/
virtual void sub_event(struct Module *module, const Event::Type &type) = 0; virtual void sub_event(struct Module *module, const Event::Type &type) = 0;
virtual void emit_event(Event event) = 0; virtual void emit_event(Event event) = 0;

View File

@ -50,6 +50,7 @@ struct ModuleThread: public interface::ThreadedThing
struct ModuleContainer struct ModuleContainer
{ {
interface::Server *server;
interface::ThreadLocalKey *thread_local_key; // Stores mc* interface::ThreadLocalKey *thread_local_key; // Stores mc*
up_<interface::Module> module; up_<interface::Module> module;
interface::ModuleInfo info; interface::ModuleInfo info;
@ -69,9 +70,11 @@ struct ModuleContainer
// post() when direct_cb becomes free, wait() for that to happen // post() when direct_cb becomes free, wait() for that to happen
interface::Semaphore direct_cb_free_sem; interface::Semaphore direct_cb_free_sem;
ModuleContainer(interface::ThreadLocalKey *thread_local_key = NULL, ModuleContainer(interface::Server *server = nullptr,
interface::ThreadLocalKey *thread_local_key = NULL,
interface::Module *module = NULL, interface::Module *module = NULL,
const interface::ModuleInfo &info = interface::ModuleInfo()): const interface::ModuleInfo &info = interface::ModuleInfo()):
server(server),
thread_local_key(thread_local_key), thread_local_key(thread_local_key),
module(module), module(module),
info(info) info(info)
@ -196,6 +199,7 @@ void ModuleThread::run(interface::Thread *thread)
cs(mc->info.name)); cs(mc->info.name));
} catch(std::exception &e){ } catch(std::exception &e){
log_w(MODULE, "direct_cb() failed: %s", e.what()); log_w(MODULE, "direct_cb() failed: %s", e.what());
mc->server->shutdown(1, mc->info.name+" failed: "+e.what());
} }
} }
{ {
@ -215,6 +219,7 @@ void ModuleThread::run(interface::Thread *thread)
mc->module->event(event.type, event.p.get()); mc->module->event(event.type, event.p.get());
} catch(std::exception &e){ } catch(std::exception &e){
log_w(MODULE, "module->event() failed: %s", e.what()); log_w(MODULE, "module->event() failed: %s", e.what());
mc->server->shutdown(1, mc->info.name+" failed: "+e.what());
} }
} }
} }
@ -495,7 +500,7 @@ struct CState: public State, public interface::Server
m_module_info[info.name] = info; m_module_info[info.name] = info;
mc = sp_<ModuleContainer>(new ModuleContainer( mc = sp_<ModuleContainer>(new ModuleContainer(
&m_thread_local_mc_key, m, info)); this, &m_thread_local_mc_key, m, info));
m_modules[info.name] = mc; m_modules[info.name] = mc;
m_module_load_order.push_back(info.name); m_module_load_order.push_back(info.name);
} }
@ -531,7 +536,7 @@ struct CState: public State, public interface::Server
} }
} }
mc = sp_<ModuleContainer>(new ModuleContainer( mc = sp_<ModuleContainer>(new ModuleContainer(
&m_thread_local_mc_key, m, info)); this, &m_thread_local_mc_key, m, info));
m_modules[info.name] = mc; m_modules[info.name] = mc;
m_module_load_order.push_back(info.name); m_module_load_order.push_back(info.name);
} }
@ -707,18 +712,73 @@ struct CState: public State, public interface::Server
return result; return result;
} }
// Call with m_modules_mutex locked
bool is_dependency_u(ModuleContainer *mc_should_be_dependent,
ModuleContainer *mc_should_be_dependency)
{
const ss_ &search_dep_name = mc_should_be_dependency->info.name;
const interface::ModuleInfo &info = mc_should_be_dependent->info;
// Breadth-first
for(const interface::ModuleDependency &dep : info.meta.dependencies){
/*log_t(MODULE, "is_dependency_u(): \"%s\" has dependency \"%s\"; "
"searching for \"%s\"",
cs(info.name), cs(dep.module), cs(search_dep_name));*/
if(dep.module == search_dep_name)
return true;
}
for(const interface::ModuleDependency &dep : info.meta.dependencies){
auto it = m_modules.find(dep.module);
if(it == m_modules.end())
continue;
ModuleContainer *mc_dependency = it->second.get();
bool is = is_dependency_u(mc_dependency, mc_should_be_dependency);
if(is)
return true;
}
/*log_t(MODULE, "is_dependency_u(): \"%s\" does not depend on \"%s\"",
cs(info.name), cs(search_dep_name));*/
return false;
}
// Throws on invalid access
// Call with m_modules_mutex locked
void check_valid_access_u(
ModuleContainer *target_mc,
ModuleContainer *caller_mc
){
const ss_ &target_name = target_mc->info.name;
const ss_ &caller_name = caller_mc->info.name;
// Access is invalid if target is caller
if(caller_mc == target_mc)
throw Exception("Cannot access \""+target_name+"\" from \""+
caller_name+"\": Accessing itself is disallowed");
// Access is invalid if target is being accessed by somebody who is
// being accessed by caller, directly or indirectly
if(!is_dependency_u(caller_mc, target_mc))
throw Exception("Cannot access \""+target_name+"\" from \""+
caller_name+"\": Not a dependency");
// Access is valid
}
bool access_module(const ss_ &module_name, bool access_module(const ss_ &module_name,
std::function<void(interface::Module*)> cb) std::function<void(interface::Module*)> cb)
{ {
sp_<ModuleContainer> mc; sp_<ModuleContainer> mc;
ss_ caller_module_name;
{ {
// This prevents module from being deleted while a reference is
// being copied
interface::MutexScope ms(m_modules_mutex); interface::MutexScope ms(m_modules_mutex);
auto it = m_modules.find(module_name); auto it = m_modules.find(module_name);
if(it == m_modules.end()) if(it == m_modules.end())
return false; throw Exception("access_module(): Module \""+module_name+
"\" not found");
mc = it->second; mc = it->second;
if(!mc)
throw Exception("access_module(): Module \""+module_name+
"\" container is null");
// Get container of current module // Get container of current module
ModuleContainer *caller_mc = ModuleContainer *caller_mc =
@ -726,15 +786,22 @@ struct CState: public State, public interface::Server
if(caller_mc){ if(caller_mc){
log_t(MODULE, "access_module(\"%s\"): Called by \"%s\"", log_t(MODULE, "access_module(\"%s\"): Called by \"%s\"",
cs(mc->info.name), cs(caller_mc->info.name)); cs(mc->info.name), cs(caller_mc->info.name));
// Throws exception if not valid.
// If accessing a module from a nested access_module(), this
// function is called from the thread of the nested module,
// effectively taking into account the lock hierarchy.
check_valid_access_u(mc.get(), caller_mc);
// Access OK; copy the caller's name so we can use it
caller_module_name = caller_mc->info.name;
} else { } else {
log_t(MODULE, "access_module(\"%s\"): Called by something else" log_t(MODULE, "access_module(\"%s\"): Called by something else"
" than a module", cs(mc->info.name)); " than a module", cs(mc->info.name));
} }
// TODO: Check that the module being accessed is a direct or
// indirect dependency of the module accessing it, and not the
// module itself
} }
// Execute callback in module thread
mc->execute_direct_cb(cb); mc->execute_direct_cb(cb);
return true; return true;
} }