Skip to end of metadata
Go to start of metadata
Icon

Work in progress

 

In Asterisk 12, we have relied on Asterisk's module architecture more than ever. While this has been good from a software architecture perspective, it has exposed some limitations of the Asterisk module loader. This page describes my ideas of what to do to improve the situation.

Module Loader Problems

Dependent modules must be configured in modules.conf

At build time, Asterisk knows the inter-module dependencies necessary for building. Unfortunately, that information isn't kept at runtime. This means that if module A depends on module B, module B must be listed explicitly in modules.conf (well, unless you turn on autoloading). Some of these dependencies are not obvious, making life difficult for users who like to load with a minimal configuration.

Modules may load even if a dependent module fails to load

If module A depends on module B, and module B fails to load due to a configuration error, module A loads anyways. It's module A's responsibility to check error codes on the module B functions it calls, but we often fail to honor that responsibility.

Module dependencies don't bump refcounts

If module A depends on module B, the user is still allowed to explicitly unload module B. The next time module A calls a module B function, hilarity ensues.

Module loading bugs tend to be inconsistent

There are a number of things which can cause failures at load time, such as taking the address of a function from another module (this requires an eager symbol resolution, even when loaded with RTLD_LAZY. Depending on which module happens to get loaded first, sometimes it works, sometimes it fails). Module loading should be at least consistent so that if there are bugs, they are easier to reproduce on different systems. Plus, consistency is usually a good thing.

Proposed Solution

Inject dependencies into ast_module_info block

When menuconfig parses the MODULEINFO comment in a module's source file, it can take that dependency information and inject it bad into the module as a -D compiler predefine. I'm not sure what format would be best, but possibly just a colon/comma separated list of dependencies would be best.

Clean up dependency data

Module dependencies are currently specified as a mix of <use type=""> and <depend> tags, but the difference is undocumented (or the documentation is on display in the bottom of a locked filing cabinet stuck in a disused lavatory with a sign on the door saying 'Beware of the Leopard'). In order to properly inject dependencies, we'll need to know which dependencies are modules, and which are libraries.

Dependency information will need to be made consistent, especially using <use type="external"> for dependencies on external libraries.

Introduce module providers

There are some features within Asterisk which may have multiple providers/implementors. Media formats could be considered a special case of this, but sorcery providers would be another interesting use case. The MODULEINFO block will need to be updated to denote when a module is a provider for an interface, and when a module uses an interface. This will ensure that all of an interface's providers are loaded before users of that interface need it.

I'm thinking something like:

Icon

My idea of injecting dependency data using menuconfig may not work out. I think it will, but if for some reason it doesn't, then dependency information will need to be duplicated in the AST_MODULE_INFO block. This is unfortunate, but we do that today with the .nonoptreq field, so meh.

 

Replace priority load order with a graph loader

Instead of ordering module loading by priority, modules would be loaded according to their specified dependencies. Here are some rough suggestions I have for the implementation.

  1. List modules in the lib/asterisk/modules directory
  2. Build module graph
    • Load module metadata lazily, to avoid loading metadata for a module you're not going to load
      • dlopen() with the RTLD_LAZY | RTLD_LOCAL flags, so that module load ordering doesn't matter
        • Normal inter-module function calls won't link, so module load ordering won't matter
        • If one module attempts to take the address of a function from another module, then dlopen() will always fail, consistently. Inter-module function pointers or global variable access must always be wrapped with a function.
        • Module functions won't be exported in the pre-initialized state
    • If autoload is enabled, then noload on a module implies noload for all modules that depend on it (and the modules that depend on them, recursively)
      • This does not apply to use types of optional or provider

      • If one of these modules that gets the implies noload is also specified in a load statement, exit with an error
    • If autoload is disabled, then load on a module implies load for all modules it depends on (and the modules that they depend on, recursively)
      • This does not apply to use types of optional or provider
      • If one of these moduels that gets the implies load is also specified in a noload statement, exit with an error

  3. Load all preload modules
    • Use the logic specified below
  4. Load the rest of the modules
    • Recursively load all of a module's dependencies that haven't been set to noload
      • If a dependency's load fails with a AST_MODULE_LOAD_DECLINE, also decline to load
      • Increment the module use count for the dependency
    • Reopen using dlopen() with RTLD_NOW | RTLD_LOCAL
      • Since we have loaded all dependencies, we can link functions now instead of lazily
      • We don't want to export functions until the module has been successfully initialized
    • Invoke the module's init function
      • If AST_MODULE_LOAD_FAILURE, exit with an error
      • if AST_MODULE_LOAD_DECLINE, return
    • If the modules is marked as AST_MODFLAG_GLOBAL_SYMBOLS, reopen with RTLD_NOW | RTLD_GLOBAL

Reopening modules

There's been a lot of mystery and confusion surrounding dlopen() and dlclose(). This is probably due to bugs in older systems we aren't likely to encounter any more. And, by experience, the work arounds put in place for buggy systems have caused Asterisk bugs on properly behaving systems.

When reopening the module, the docs (and observed behavior) tell me that we should first open the module, and then close the new handle. dlopen() and dlclose() refcounts the modules, and will change module flags on multiple dlopen(). This avoids unnecessary churn running load/unload code in the modules simply to change flags.

Loading/unloading at runtime

When a module is loaded at runtime, it should recursively load any modules it depends on (excluding optional and provider dependencies). It should also bump the use count for any module it depends on.

Conversely, when a module is unloaded it should recursively unload any modules that depend on it (excluding optional and provider dependencies). Unloading decrements the use count for any module it depends on.

  • No labels

3 Comments

  1. Instead of injecting dependency data from menuselect, what if MODULEINFO were merged into xmldocs?  For example:

    <module name="res_sorcery_example" description="This is an example sorcery provider" support_level="core" loadpriority="default" globals="false">
            <use type="provider">sorcery</provides>
                    <application ...</application>
                    ...
    </module>

    This is what I propose as a module's output to core-en_US.xml.  I think the MODULEINFO block should be kept separate in the source.  The documentation block would be injected in the module element.  Probably make loadpriority / globals optional attributes.

    This would remove a bunch of parameters from AST_MODULE_INFO blocks.  The loader would build a complete dependency tree before ever running dlopen, and would never have to "reopen" modules to change flags.  Loading the dialplan "exten => s,1,Gosub(something())" could autoload app_stack.so.  I think it's important that modules.conf be able to replicate the existing behaviour of autoload=no (maybe autoload=never?).

    The down side, correct xmldocs would be required for all modules.  We'd need to ensure that 3rdparty modules have an easy way to install xmldocs.

    As for reopening modules to export globals after initialization, I don't think we should do this.  The loader should be able to trust that the documented dependencies are accurate, and nothing will use the exported globals until it's appropriate.  If res_consumer uses globals from res_provider, the <use> statements will ensure that res_consumer isn't loaded until after res_provider is initialized.  Consumers would need to manually obtain references to any optional module dependencies before using those globals.

    1. The down side, correct xmldocs would be required for all modules. 

      That's a downside? (big grin)

      It's an interesting thought, though. There are tradeoffs to the current mechanism of self-describing modules, versus keeping the data outside the module.

      The downside of self describing modules is that you have to load the module to know how to load the module, which could mean unloading it so it can be reloaded properly. Doing this consistently across all the platforms that Asterisk supports has been problematic in the past.

      The downside of external metadata is that there's always the problem of keeping the data synchronized; of making sure that the metadata file is consistent with the binary.

      I don't know if keeping the metadata in a single file is a good idea, though. Taking the metadata from all the modules and loading it into a single module is... anti-modular.

      There are ways of working around these downsides, but those all have their own trade offs.

      Thanks!

      1. For the single file of metadata, I'm not sure if this is good or bad.  I think it would be fine for stuff from asterisk source tree.  We'd have to be sure there is a way that res_custom_module can come with a separate xmldoc file that gets merged at startup.  Having stuff from the asterisk source tree use separate files would be a nice goal eventually but currently can be a problem.  For example if you don't load xmldoc for app_macro, you will get an XPointer error for WaitExten (an app in the pbx core).  For now at least we should keep producing core-en_US.xml for the entire source tree, support loading additional docs from /var/lib/asterisk/documentation/thirdparty.

        One thought on safety, it might be possible to inject MD5 of the module source files as a compiler declare, pass it to ast_module_register so it can be checked against xmldocs.  This would increase the difficulty of producing 3rd party modules, but not by much.  I guess we could use an optional attribute so md5 is checked if it's present.