Component System

Ben Langfeld edited this page Apr 11, 2012 · 10 revisions

DEPRECATION NOTICE: This is old documentation relevant to Adhearsion 1.x and will soon be removed. See the main documentation for up-to-date info.

The component system is designed to provide an easy interface for developing plug-ins for Adhearsion that can be easily shared with the Adhearsion community. You can see a community member discuss their implementation experience here. Starting with Adhearsion 1.0, components may be installed via RubyGems. Learn more about Gem-based components here.

##Creating a blank component##

Start by running the "ahn" shell command. Doing so inside of an existing Adhearsion application will put the skeleton component into the components/ directory. Doing it outside of an Adhearsion app puts it into the current directory. In both cases the resulting skeleton can be easily packaged into a gem.

ahn create component mycomponent

This will create the directory "mycomponent" with an example library file and an example YAML configuration file.

##Scoped methods##

# components/my_component/my_component.rb

initialization do
  COMPONENTS.my_component["server"] = MyCoolSpecialServerClass.new
end

methods_for :dialplan do
  def countdown
    COMPONENTS.my_component["server"].introduce(first, second)
  end
end

methods_for :dialplan, :generators do
  def log_something

  end
end

methods_for :rpc do

  delegate :originate, :to => COMPONENTS.my_component[:server]

  def introduce(first, second)

  end
end

methods_for :events do

end

Methods defined in a scope will have full access to other methods in that scope. For example, methods_for(:dialplan) can execute dialplan methods directly via play(), menu(), etc. The object in which they're executed is actually the Call.

##Finding method scopes##

Below is a list of currently supported method scopes but this list may change. To get the latest list for your particular installation, type rake method_scopes

  • :dialplan
  • :events
  • :generators (note: this is not used yet.)
  • :rpc - Remote Procedure call
  • :global

##Referencing methods outside of your scope##

Let's say you want your :dialplan method to also be shared by :rpc. In this scenario, consider that the calling environment of these methods will be very different. Methods may appear to be the same but actually behave completely differently. For example, consider that you have a method shared in both :dialplan and :rpc named foo and it invokes the hangup method. When called via :rpc your Adhearsion app must specify a call or channel to hangup. However, in :dialplan it almost certainly assumes you mean to hangup the current call.

##Method conflicts##

All recognized method conflicts will raise warnings when the Adhearsion application initializes. The actual precedence will be handled by Ruby itself.

##Constants##

When you define a constant in a component, it will be available in the root namespace. A constant in Ruby begins with a capital letter.

Because a constant in the root namespace is globally accessible, you can use the constant in any methods you define in your scopes:

# components/constants_are_cool/constants_are_cool.rb
MyClientObject = SpecialClientObject.connect "localhost"

methods_for :dialplan do
  def send_greeting
    MyClientObject.send_message :greet
  end
end

If you come from other programming languages and feel inclined to use a global variable, try to accomplish the global availability with a constant. They work virtually the same and are more safe for programming with threads.

##Instance/class variables##

Do not use instance or class variables. An instance variable is a variable whose name begins with "@" and a class variable is one which begins with "@@". You should be careful when using them because the methods you define will be executed in many different objects and classes. If you must share state between method invocations, either pass it around in the function arguments, define root-level constants, or store it in your components' configuration. Always consider thread safety when you're using any shared resources in separate threads (see next section).

##You're writing multi-threaded code!##

Please keep in mind you may introduce a race condition into your application inadvertently due to resources being in awkward states. For example, this would generate a race condition:

methods_for(:events) do
  def redefine_foobar
    # You should never be *this* dynamic. This is just a contrived example.
    metaclass.remove_method(:foobar)
    meta_def(:foobar) { doing_something }
    foobar
  end
end

Because the redefine_foobar method can be called from different threads, two threads may try to execute it and find that the "foobar" method does not exist when one tries to run it because another thread has temporarily removed it. A way to solve this would be something like this:

FOOBAR_LOCK = Mutex.new

methods_for(:events) do
  def redefine_foobar
    # Synchronizing makes this code Thread-safe
    FOOBAR_LOCK.synchronize do
      # This Ruby is kinda complex. If you don't understand it, don't worry.
      metaclass.remove_method(:foobar)
      meta_def(:foobar) { doing_something }
      foobar
    end
  end
end

This will ensure only one Thread can execute foobar at a time. If you can, try to avoid sharing state or only share constants which are frozen().

##Configuration##

Every component has its own YAML configuration file named after the component which should be in the component's directory. If the component's name is "phone_hero", the configuration filename would be "phone_hero.yml".

Example configuration file:

# This is a comment
company_name: Apple, Inc.
stock_symbol: APPL

Accessing configuration within your component

Because of the functional nature of the new component system, all functions must refer to their own configuration globally. The root-level constant COMPONENTS is available as syntax sugar. Below is an example:

# kewlz0rz_component.rb

methods_for(:generators) do
  def public_company
    name   = COMPONENTS.kewlz0rz_component["company_name"]
    symbol = COMPONENTS.kewlz0rz_component["stock_symbol"]

    some_special_method_that_does_something name, symbol
  end
end

Warning: Do NOT load your configuration file manually. Your component may be installed via a gem or some other means and not kept in the same directory as your config file.

##Reusing code via the :rpc scope##

Writing methods in the :rpc scope can be a great way to have other processes reuse the code assets you developed for your Adhearsion application. If you want to generate HTML documentation for the :rpc methods exposed in your Adhearsion application, type rake doc:rpc. This will create a doc-rpc folder of YARD-generated docs.

These methods can be invoked via DRb using Adhearsion's internal DRb support. Some components (such as the restful_rpc component) expose Adhearsion's internal RPC system to new protocols.

##Directory structure##

The directory structure is simple. Given the component name "killer_tofu", the directory structure for a newly created component would look like the following:

  • components/killer_tofu/killer_tofu.yml
  • components/killer_tofu/killer_tofu.rb

Note: a more sophisticated "killer_tofu" component may have the following structure:

  • components/killer_tofu/killer_tofu.yml
  • components/killer_tofu/lib/killer_tofu.rb
  • components/killer_tofu/lib/tofu_eater.rb
  • components/killer_tofu/spec/killer_tofu_spec.rb
  • components/killer_tofu/spec/tofu_spec.rb
  • components/killer_tofu/spec/lethality_spec.rb

##Managing lifecycles of servers and objects##

Because OOP is a useful paradigm for software development, its availability is not compromised in the new component system, despite its scoped method-based paradigm. Also, you may want your component to start up new servers.

###Don't forget to shutdown your resources!###

If you start a server, you should register a shutdown hook. For example:

initialization do
  host, port = COMPONENTS.micromenus.values_at "host", "port"
  ::MICROMENUS_SERVER = MyServer.start(host, port)
  Events.register_callback("/shutdown") { ::MICROMENUS_SERVER.stop }
end

##Using the logging system##

The Adhearsion logging system is meant to be very easy for virtually all parts of the framework to log without much friction. Every log message is scoped in one of two ways:

  • Namespace
  • Logging level

Both of these are optional. Here's an example omitting both of these:

ahn_log "Testing 123"

The example above uses the :info logging level at the root level namespace. The example below uses the :error logging level at the root level logging namespace:

ahn_log.error "Warning Will Robinson!"

Namespaces can be arbitrarily invoked on the ahn_log object. They'll be created the first time you use them. The following example uses the :info logging level in the kewlz0rz_stuff namespace:

ahn_log.kewlz0rz_stuff "Starting kewlz0rz stuff"

This uses the :error logging level in the kewlz0rz_stuff namespace.

ahn_log.kewlz0rz_stuff.error "Couldn't start kewlz0rz_stuff!"

This shows arbitrarily nested namespaces:

ahn_log.lolcats.icanhascheezburger.warn "TONIGHT WE NOM IN HELL!"

Note: Doing ahn_log.error.kewlz0rz_stuff is invalid. If the logging level is specified at all, it must be the last method.

##Unit Testing Components##

The component system has a built-in testing framework which lets you provide quality control over these crucial aspects of your framework. It uses RSpec and Flexmock.

##Example spec##

Adhearsion has built-in RSpec helper classes and modules which can make your life easier when testing your components. The best way to learn how to use it is by example. When you create an application, take a look at the components/disabled/restful_rpc/spec/restful_rpc_spec.rb file for an example of a well-tested component.

##How does it work?##

This new component system is a great example of Ruby's dynamism and power; methods are completely tangible and manipulable.

The methods_for() method instantiates a new anonymous Module by doing Module.new and then module_evals the block given to it in that module. It then gives Adhearsion's ComponentMethodScopeManager the Module and tells it which Symbols the particular Module is for.

##Enabling/Disabling components##

You can run the following command:

ahn disable component <ComponentName>

which will simply move your component to the components/disabled folder, creating it if it doesn't exist.

ahn enable component <ComponentName>

will move it back to components/

##Method-based design rationale##

The most significant difference over the old (pre-Adhearsion-0.8.0) component system is the removal of strict OOP. This was done for a few reasons:

  • When someone wishes to simply add a new dialplan method, it's unnecessary to encapsulate that into an object
  • When forcing everyone to instantiate classes that only exist as a framework requirement, the chosen variable names assigned to these objects are meaningless and don't carry any additional meaning over the function name. For example: HelpCommand.help
  • When exposing components to the world via distributed computing, custom objects are not really an option