Skip to main content

Notice: this Wiki will be going read only early in 2024 and edits will no longer be possible. Please see: https://gitlab.eclipse.org/eclipsefdn/helpdesk/-/wikis/Wiki-shutdown-plan for the plan.

Jump to: navigation, search

Difference between revisions of "Aperi Data Server Design R2"

(Extension Point Definition)
Line 1: Line 1:
 
 
== Introduction ==
 
== Introduction ==
  
The Data Server does not have a good story for extensibility. Such represents a severe limitation for those looking to build a complete application on top of Aperi (for example, TPC). Currently, extensibility is a compile-time consideration. To extend functionality, one must edit classes within our base, defining new numeric constants and updating existing static arrays. While it works, long-term viability of Aperi requires that we retire that approach. As such, the goal of this work-item is the implementation of runtime extensibility for the Data Server.
+
The Aperi Data Server does not have a good story for extensibility. Such represents a severe limitation for those looking to build a complete application on top of Aperi (for example, TPC). Currently, extensibility is a compile-time consideration. To extend functionality, one must edit classes within our base, defining new numeric constants and updating existing static arrays. While it works, long-term viability of Aperi requires that we retire that approach. As such, the goal of this work-item is the implementation of runtime extensibility for the Data Server.
  
 
We need to add runtime extensibility to the following areas of the Data Server:
 
We need to add runtime extensibility to the following areas of the Data Server:

Revision as of 18:45, 7 September 2006

Introduction

The Aperi Data Server does not have a good story for extensibility. Such represents a severe limitation for those looking to build a complete application on top of Aperi (for example, TPC). Currently, extensibility is a compile-time consideration. To extend functionality, one must edit classes within our base, defining new numeric constants and updating existing static arrays. While it works, long-term viability of Aperi requires that we retire that approach. As such, the goal of this work-item is the implementation of runtime extensibility for the Data Server.

We need to add runtime extensibility to the following areas of the Data Server:

  • Service Infrastructure
  • Scheduler Infrastructure
  • Resource Infrastructure
  • Alert Infrastructure

The Data Server is fairly complex. So rather than attempting to catalogue all of the changes we must make to achieve runtime extensibility, this document will focus on the steps we need to take to reach our goal. The addition of runtime extensibility to the Data Server will be an iterative process. At a high-level, for each area in which we seek to add runtime extensibility, that process will involve the following:

  • Extension point definition
  • Extension infrastructure implementation
  • Extension creation
  • Extension infrastructure migration

The four points presented above are discussed in the sections that follow. Putting together runtime extensibility will involve continual refinement. Implementing it using an iterative approach should allow us to maintain a high-level of functionality throughout the transition, though a fair amount of testing will be required to verify that nothing has gone awry after we exit our development phase. Given that our goal is to use extensions for just about everything, the various components of the Data Server will serve as a good test bed for its runtime extensibility implementation. Putting together documentation and examples should be fairly straightforward. However, to demonstrate real / tangible value to TPC, we may want to show how they would go about bringing back some of the functionality we removed as part of our first phase of development. (A good example might be database support, as its implementation spans the server, agent, and GUI.)

Development Methodology

This section outlines that the process that will be used to add runtime extensibility to the Data Server. It covers, in some detail, the four activities presented in the introduction. It does not, however, go into a lot of specific detail about exactly how our code will be transformed. As it stands, the Data Server sits at the center of Aperi, interacting with the GUI, the Device Server, and the Agent. As changes are made, it will be extremely important to keep affected parties in the loop. Significant updates will be accompanied by documentation summarizing changes and discussing how Aperi components besides the Data Server might be affected.

Extension Point Definition

This will involve defining extension points. Additionally, in cases when such is necessary, corresponding metadata classes will be defined. Definitions will map directly to the entities / structures currently defined in our code. For example, the alert condition extension point schema will look something like this:

Aperiextpoints.jpg

<?xml version='1.0' encoding='UTF-8'?>
<!-- Schema file written by PDE -->
<schema targetNamespace="org.eclipse.aperi.common">
   <annotation>
      <appInfo>
         <meta.schema plugin="org.eclipse.aperi.common"
            id="alertConditions" name="Alert Conditions"/>
      </appInfo>
      <documentation>
         [Enter description of this extension point.]
      </documentation>
   </annotation>

   <element name="extension">
      <complexType>
         <sequence>
            <element ref="alertCondition" minOccurs="1" maxOccurs="unbounded"/>
         </sequence>
         <!-- Attributes ... -->
      </complexType>
   </element>

   <element name="alertCondition">
      <annotation>
         <documentation>
            This element represents an alert condition. It defines an
            appropriate structure for metadata specification. Some notes: 1)
            Though not absolutely required, the number of e-mail parameters 
            specified for an alert condition should match the number of script 
            parameters. 2) The order in which e-mail parameters is specified is 
            not important. The same is not true for script parameters. The 
            order of the arguments passed to a script kicked off in response to 
            a given alert will mirror the order in which script parameters are 
            specified for the alert condition extension associated with that 
            alert.
         </documentation>
      </annotation>
      <complexType>
         <sequence>
            <sequence minOccurs="0" maxOccurs="unbounded">
               <element ref="emailParameter"/>
            </sequence>
            <!-- Additional sequences ... -->
         </sequence>
         <attribute name="emailMessage" type="string">
            <annotation>
               <documentation>
                  The message ID of the e-mail message associated with this 
                  alert condition. This attribute replaces the following static 
                  array: AlertDefinition.emailMsgID[]. NOTE: 
                  We will need to update our infrastructure so that it is 
                  possible to specify the resource bundle associated with a 
                  given message ID.
               </documentation>
            </annotation>
         </attribute>
         <attribute name="emailSubject" type="string">
            <annotation>
               <documentation>
                  The message ID of the e-mail subject associated with this 
                  alert condition. This attribute replaces the following static 
                  array: AlertDefinition.emailSubject[].
               </documentation>
            </annotation>
         </attribute>
         <!-- Additional attributes ... -->
      </complexType>
   </element>

   <element name="emailParameter">
      <annotation>
         <documentation>
            This element represents and e-mail parameter. It is defined to 
            replace the following static array AlertDefinition.emailParms[].
         </documentation>
      </annotation>
      <complexType>
         <!-- Attributes ... -->
      </complexType>
   </element>

   <!-- Additional elements ... -->
   <!-- Annotation (documentation) ... -->
</schema>

Its corresponding metadata class will look something like this:

public class AlertConditionMetadata {
    // Constants ...

    // Replace AlertDefinition.emailMsgID[]
    private String _emailMessageId;
    
    // Replace AlertDefinition.emailSubject[]
    private String _emailSubjectId;
    
    // Replace AlertDefinition.emailParms[]
    private AlertEmailParameter[] _emailParameters;
    
    // Additional instance variables ...
    // Constructors / Accessors / Mutators ...
}

The important thing to note in the above example is how existing Data Server constructs will be translated into extension points and metadata classes. For instance, AlertDefinition.emailMsgID[] is a static array that associates e-mail text with alert conditions. Since every condition has associated e-mail text, it makes sense to create a corresponding extension point attribute and metadata instance variable. Such is done above. Then, with the runtime extensibility infrastructure in place, instead of accessing a static array when looking for e-mail text, we'll perform a registry lookup, obtain a metadata object, and invoke a simple accessor method.

The use of numeric constants in the Data Server contributes to the weakness of its extensibility model. String constants provide greater flexibility. Initially, when defining extension points and associated metadata classes, we will not attempt to move away from the use of numeric constants. Avoiding immediate migration away from numeric constants will allow us to start using extensions without changing a large amount of code. (For example, simply changing the type of the typeCode instance variable in org.eclipse.aperi.request.Request from short to string results in approximately 200 errors.) Following the iterative model, once we have basic extension-based extensibility in place, we'll make an additional pass through the code to move to string constants. Doing so will likely require updating various metadata classes.

The space of the attribute types that can be used within an extension point schema is extremely limited. An attribute can be either a boolean, string, class, or resource. (The boolean, string, and class type designations are straightforward. At this point, it is not clear to me constitutes a resource attribute.) Our extension points will involve attributes that do not directly fall into any of those classes. We'll need to deal with them in a reasonable fashion. Example? The alert condition extension point has a couple of byte-long bit flag attributes. Approach? Represent them as a string sequence of eight 0s and 1s, and parse values appropriately in the associated extension manager. (Alternatively, we might restructure the code such that use of things like byte flags isn't required. It's an option we can consider, but won't stand as the planned course of action as we attempt to minimize changing how the code works while taking an initial stab at runtime extensibility.)

Extension Infrastructure Implementation

An extension point manager will be needed for each extension point. But each extension point will not have its own manager. Instead, there will be some consolidation. For example, implementing runtime extensibility for the service infrastructure will require that extension points be defined for service providers and request handlers. However, there will be only be a single extension point manager for the service infrastructure, responsible for dealing with all service provider and request handler extensions.

Extension point manager implementations will follow the after the AbstractExtensionMgr class. They will 1) be responsible for extension cache management and 2) facilitate access to the attributes associated with registered extensions, performing whatever conversion may be required (for example, string -> byte-long bit flag).

Following the discussion surrounding numeric vs. string constants from the previous section, multiple extension point manager update passes will likely be required, the second one coming with the transition from numeric to string constants. For example, it will be necessary to remove string -> short conversions initially put in place for IDs.

Extension Creation

After the necessary extension points are defined and the associated support infrastructure is put in place, it will be time to define actual extensions. Extensions will be created for each extension point and will map to the entities defined within the Data Server. Example? The service provider extension point schema might look something like this:

The corresponding Aperi extension might look like this:

<extension
      id="org.eclipse.server.ServiceProviders"
      name="Aperi Server Service Providers"
      point="org.eclipse.aperi.common.serviceProviders">
   <serviceProvider
         class="org.eclipse.aperi.server.Svp"
         description="typeCode = RequestCode.SERVER (1)"
         name="Server Service Provider"
         typeCode="1"/>
   <!-- Additional service providers ... -->
</extension>

Similar to what will be required for extension point definition and extension infrastructure implementation, an additional iteration (performed after we've reached some level of comfort with the Data Server running with extensions), involving the transition from numeric to string constants will require that we revisit our extensions. For example, the service provider extension presented above might eventually be transformed into this (notice the different value associated with the 'typeCode' attribute):

<extension
      id="org.eclipse.server.ServiceProviders"
      name="Aperi Server Service Providers"
      point="org.eclipse.aperi.common.serviceProviders">
   <serviceProvider
         class="org.eclipse.aperi.server.Svp"
         description="typeCode = RequestCode.SERVER (org.eclipse.aperi.server.serviceProvider.Server)"
         name="Server Service Provider"
         typeCode="org.eclipse.aperi.server.serviceProvider.Server"/>
   <!-- Additional service providers ... -->
</extension>

Extension Infrastructure Migration

Moving to the extension-based runtime extensibility infrastructure will be a multi-staged process. The first stage will occur after the initial definition of extension points, the initial implementation of extension managers, and the initial creation of extensions. The goal of this stage will be to eliminate access to, and the definition of, static arrays. Following completion of this stage, basic extension-based runtime extensibility will be in place for the Data Server. (Note that should we run out of time, theoretically, we might be able to call it quits with the basic runtime extensibility just described.)

The second stage will focus on enhancing the flexibility of the extension-based runtime extensibility infrastructure by migrating from the numeric to string constants. This will involve doing the following work (much if which was mentioned in previous sections):

  • Update extension point schemas and associated metadata classes.
  • Update extension managers.
  • Update extensions.
  • Update variable types and resolve compilation errors.

Our database schema and associated SQL queries depend upon the use numeric constants. Changing our schema and updating our queries, in conjunction with the transition from numeric to string constants, might seem like a reasonable thing to do. However, it would 1) add to what is already a fair amount of work (likely exposing us to additional risk), and 2) be a potential source of problems for TPC's eventual adoption of Aperi. As part of the second stage, instead of making significant database-related changes, we will put together a simple registry mechanism that allows us to map from string to numeric constants for use in the backend, for those entities which need to be stored in our database (for example, resource attributes and alerts). Clients of our runtime extensibility infrastructure will not need to be aware of what's going on behind the scenes. The registry mechanism will consist of a single database table and a single class. A prototype definition of the table looks like this:

----------------------------------------
-- DDL statements for table "T_REGISTRY"
----------------------------------------

CREATE TABLE "T_REGISTRY" (
    "NAME" VARCHAR(512) NOT NULL,
    "ID" {0} NOT NULL) {22}
;

-- DDL statement for primary key on table "T_REGISTRY"

ALTER TABLE "T_REGISTRY"
    ADD PRIMARY KEY
        ("NAME")
;

-- DDL statement for index on table "T_REGISTRY"

CREATE UNIQUE INDEX "T_REGISTRY_ID_IDX" ON "T_REGISTRY"
	("ID" ASC)
;

Here is a prototype implementation of the associated class:

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;

import org.eclipse.aperi.infrastructure.database.AutoIdentifier;
import org.eclipse.aperi.server.Server;

/**
 * This class contains utility methods to support the integer to string mapping
 * required by Aperi. It is backed by the T_REGISTRY table. It is used to support
 * string-based extensibility while maintaining the database integrity (in
 * particular, allowing us to avoid storing the same string in our database
 * multiple times).  
 */
// TODO Make it possible to add new entries to registry without going through accessor?
// TODO Make it possible to avoid automatic entry creation if entry not found?
public class AperiRegistry {
    private static Map<String, Integer> _idLookupCache = new HashMap<String, Integer>();
    private static Map<Integer, String> _nameLookupCache =
        new HashMap<Integer, String>();

    private static Object dbLock = new Object();
    
    public static int getId(String name) {
        // Check cache for ID associated with name
        Integer id = _idLookupCache.get(name);
        
        if (id == null) {
            synchronized (dbLock) {
                // Check cache for ID associated with name
                // Cache may have been updated while thread was awaiting lock
                id = _idLookupCache.get(name);
                if (id == null) {
                    Connection c = null;
                    ResultSet resultSet = null;
                    PreparedStatement query = null;
                    PreparedStatement insert = null;
                    
                    // ID not present in cache
                    // Check database registry
                    try {
                        c = Server.getConnection();
                        
                        query = c.prepareStatement
                            ("select ID from T_REGISTRY where NAME = ?");
                        query.setString(1, name);
                        
                        resultSet = query.executeQuery();
                        if (resultSet.next()) {
                            // Found ID in database registry
                            // Update cache for future calls
                            id = resultSet.getInt("ID");
                            _idLookupCache.put(name, id);
                            _nameLookupCache.put(id, name);
                        } else {
                            // Did not find ID in registry
                            // Generate ID and update database registry
                            id = AutoIdentifier.getIdentifier
                                (AutoIdentifier.RESOURCE_ID, 1);
                            
                            insert = c.prepareStatement
                                ("insert into T_REGISTRY values (?, ?)");
                            insert.setString(1, name);
                            insert.setInt(2, id);
                            
                            insert.executeUpdate();
                            c.commit();
                            
                            // Update cache for future calls
                            _idLookupCache.put(name, id);
                            _nameLookupCache.put(id, name);
                        }
                    } catch (SQLException e) {
                        try { if (c != null) c.rollback(); }
                        catch (SQLException sqle) { /* ignore */ }

                        throw new RuntimeException
                            ("Failed to obtain ID associated with '" + name + "'", e);
                    } finally {
                        try { if (resultSet != null) resultSet.close(); }
                        catch (Exception e) { /* ignore */ }
                        
                        try { if (query != null) query.close(); }
                        catch (Exception e) { /* ignore */ }
                        
                        try { if (insert != null) insert.close(); }
                        catch (Exception e) { /* ignore */ }
                        
                        try { if (c != null) c.close(); }
                        catch (Exception e) { /* ignore */ }
                    }
                }
            }
        }
        
        return id;
    }
    
    public static String getName(int id) {
        // Check cache for name associated with ID
        String name = _nameLookupCache.get(id);
        
        // Name not present in cache
        // Check database registry (we're only reading so we don't need lock)
        Connection c = null;
        ResultSet resultSet = null;
        PreparedStatement query = null;
        try {
            c = Server.getConnection();
            
            query = c.prepareStatement("select NAME from T_REGISTRY where ID = ?");
            query.setInt(1, id);
            
            resultSet = query.executeQuery();
            if (resultSet.next()) {
                // Found name in database registry
                // Update cache for future calls
                name = resultSet.getString("NAME");
                _nameLookupCache.put(id, name);
                _idLookupCache.put(name, id);
            } else throw new RuntimeException("ID " + id + " does not exist");
        } catch (SQLException e) {
            throw new RuntimeException
                ("Failed to obtain name associated with ID " + id, e);
        } finally {
            try { if (resultSet != null) resultSet.close(); }
            catch (Exception e) { /* ignore */ }
            
            try { if (query != null) query.close(); }
            catch (Exception e) { /* ignore */ }
            
            try { if (c != null) c.close(); } catch (Exception e) { /* ignore */ }
        }
        
        return name;
    }
}

With the move from numeric to string constants, queries involving numeric constants will need to use the registry to map from string constants. Additionally, our installation process inserts numeric constants into the database when it is created. Consider, for example, the following statement

-- ####################################################################
-- # INSERT STATEMENTS FOR DATA TABLES
-- # Add new rows for new discovery schedule and alert types
-- ####################################################################
-- # CIMOM discovery failed alert
INSERT INTO "T_ALERT_DEFINITION"
  VALUES (110, '{109}', '{112}', 
          '{113}', 0,
          '{109}', '1', 0, 8, 26, '0', -1, '0', '0',
          '0', '8', '0', {9}, -1, {9},'0','0')
;

In the insert statement above, the 8th through 10th value parameters (i.e., 0, 8, and 26) correspond to constant product ID, alert type, and alert condition values. With the move from numeric to string constants, it will no longer be possible to hardcode such values. As such, we will need to update our database schema creation process to leverage the registry. Appropriate changes will be made to org.eclipse.aperi.install.SilentDatabaseInstall as part of the work done for the Data Server. Potential solution? Instead of using constants, we'll generate appropriate values at installe time. We'll replace such constants with appropriate markers, indicating that we should use our ID generator (AutoIdentifier) to produce a unique ID. With such changes in place, the insert statement above might look like this:

INSERT INTO "T_ALERT_DEFINITION"
  VALUES (110, '{109}', '{112}', 
          '{113}', 0,
          '{109}', '1', {200,alertProductString}, {200,alertTypeString}, {200,alertConditionString}, '0', -1, '0', '0',
          '0', '8', '0', {9}, -1, {9},'0','0')
;

The '200' would be a signal to use the registry. The additional parameter would be the string used to perform the lookup against the registry (and, if necessary, generate an ID).

Following completion of the second stage, the Data Server will be in fairly good shape with respect to runtime extensibility. However, there will still be work to do. Areas on which we'll be in a better position to focus include the following:

  • Componentization. For example, with a good extensibility story in place for our service providers and request handlers, it might make sense to move our Data Server service infrastructure code from the org.eclipse.aperi.common bundle to a separate bundle.
  • Consolidation. For example, the Data Server and Data Agent appear to share the notions of service providers and request handlers. However, their implementations are distinct. Infrastructure consolidation wouldn't bring any short-term benefits. However, long-term, it would likely 1) ease maintainability, and 2) contribute to simplifying the learning curve associated with building an application on top of Aperi.

Once we go beyond the second stage of extension infrastructure migration discussed above, we'll enter a stage conducive to incrementally refining our extensibility story in the 'Eclipse' (that is, aggressive refactorization).

Back to the top