• Publicity: Public Only All

06-param-procs.tcl

xotcl-core implementation for OpenACS package parameters. This functionality was backported to acs-tcl in OpenACS 5.10. The functions here are just for backward compatibility, in case these functions were called directly.

Location:
packages/xotcl-core/tcl/06-param-procs.tcl
Author:
Gustaf Neumann <neumann@wu-wien.ac.at>

Procedures in this file

Detailed information

[ hide source ] | [ make this the default ]

Content File Source

::xo::library doc {

  xotcl-core implementation for OpenACS package parameters.

  This functionality was backported to acs-tcl in OpenACS 5.10. The
  functions here are just for backward compatibility, in case these
  functions were called directly.

  @author Gustaf Neumann (neumann@wu-wien.ac.at)
}

# Motivations:
#
#  - Huge number of parameter_values in larger dotlrn installations
#    Learn: currently > 0.3 mio entries,
#    Galileo: > 2mio (2nd most frequent kind of object type)
#    Small oacs installations: 1000 objects (38 package instances)
#
#  - High growth, when parameters are used more intensively
#    Size grows quadratically: #parameter * #package_instances,
#     independent of changed parameter values
#    -> does not scale well.
#
# - High degree of redundancy:
#   Most parameters are stored multiple times with the same values
#   (e.g. most dotlrn parameters > 4000 times on dotlrn; cause:
#   Cause: high number of communities.
#
#   Do we really need to store 4000 times what the pretty-plural
#   string is one and the same string?
#
# - Most parameter_values are identical to default values
#   For 1 parameter in learn, we have 8 different values, for
#   4 parameters we have 3 different values, ... for most,
#   all values are the same
#
# - Huge space improvements, when redundancy is removed.
#   Learn: from 300000 entries -> 406 necessary entries
#   Small oacs installation: 1000 objects -> 256 necessary entries
#   => especially big savings on larger installations.
#
# Other shortcomings:
#
# - Existent design is 2 level:
#     package-key provides default
#     package-instance keeps values (materialized cross-product)
#
# - Consequences
#   1) Since default values are copied into
#      per-package-instance-values altering the default has no
#      immediate effect.  It would be nice to alter in an OpenACS
#      installation e.g. the default-values for all forums for a
#      certain parameter, and that this value is used in cases, where
#      the admin has not changed the package parameters
#
#   2) No inheritance between packages is possible.  It would be nice
#      to define derived packages (such as e.g. s5 derived from
#      xowiki) where the parameters do not have to be duplicated
#      (e.g. a new parameter added to xowiki should be available in
#      the s5 package as well, otherwise code reuese is limited)
#
# ======================================================================
#
# The implementation below addressed these issues (i.e. is much more
# flexible) and is substantially faster (current implementation):
#      parameter get_from_package_key   old: 172.92 new: 32.16 (5x)
#      parameter get                    old: 63.09 new: 31.29 (2x)
#
# The implementation uses the OpenACS datamodel (apm_packages,
# apm_package_values) and loads the parameters during startup.
#
# Missing:
#   - definition of new parameters (based on ::xo::db interface)
#   - changing of per-package-key values
#   - user interface
#   - alternate permissions for changing/deleting per-package-instance and
#     per-package-key values (simple approach: use swa for the latter)
#
# ======================================================================
#
# Illustrative example for lookup logic
#
# Package class hierarchy
#
#   ::xo::Package (apm_package)
#      <- ::xowiki::Package
#           <- ::s5::Package
#
#   package_parameter:
#       parameter_id  package_key  parameter_name         default_value
#        835          xowiki       with_yahoo_publisher     0
#       2071          s5           with_yahoo_publisher     0
#
#   apm_packages:
#       package_id  parameter_id  attr_value
#       2075        2071          0
#
# Lookup for package_id=2075 "with_yahoo_publisher"
#   1) lookup parameter_id for "with_yahoo_publisher" from s5 (::s5::Package)
#      1.1) parameter_id exists for s5 => parameter_id=2071
#           lookup value for parameter_id=2071,package_id=2075
#           1.1.1) value for parameter=2071 and package_id=2075 exists
#                  => return value
#           1.1.2) value for parameter=2071 and package_id=2075 does not exist
#                  => return default value for parameter and package_key=s5
#      1.2) no parameter_id for s5 + "with_yahoo_publisher"
#           search for parameter_id in superclasses ...
#
#   2) lookup parameter_id for "with_yahoo_publisher" from superclass
#      2.1) parameter_id exists for xowiki => parameter_id=835
#           lookup value for parameter_id=835,package_id=2075
#           2.1.1) value for parameter=835 and package_id=2075 exists
#                  => return value
#           2.1.2) value for parameter=835 and package_id=2075 does not exist
#                  => return default value for parameter and package_key=xowiki
#      2.2) no parameter_id for xowiki + "with_yahoo_publisher"
#           search for parameter_id in superclasses ...

namespace eval ::xo {

  Class create ::xo::parameter

  # Every OpenACS parameter should work with the methods defined here.
  # So, fetch first the apm_parameter class from the definitions
  # in the database, and ...
  ::xo::db::Class get_class_from_db -object_type apm_parameter

  #
  # Complete attribute definition in acs_attributes
  #
  ::xo::db::apm_parameter slots {
    ::xo::db::Attribute create description  \
        -datatype string -sqltype varchar(2000) \
        -pretty_name "Description"
    ::xo::db::Attribute create section_name \
        -datatype string -sqltype varchar(200) \
        -pretty_name "Section Name"
    ::xo::db::Attribute create datatype \
        -datatype string -sqltype "varchar(100) not null" \
        -constraint_values [list number string text] \
        -default "string" \
        -pretty_name "Datatype"
    #
    # TODO: Constraint_values are dummies for now.
    #
    # Should be for db::Attributes:
    #    constraint apm_parameters_datatype_ck
    #    check(datatype in ('number', 'string','text')),
    #
    # Could be used directly for UI selections as well.
    #
    #
    # Complete some slot definitions:
    #
    # TODO: the following two settings making package_key and
    # default_value required are semantically correct. However, this
    # prohibits that apm_parameters can be created via ::xo::db::Class
    # instantiate_objects, since this functions tries to create
    # objects first without parameters.
    #
    #package_key   configure -required true
    #default_value configure -required true

    section_name  configure -default ""
  }

  # ... add the methods of ::xo::parameter by adding this as a mixin
  ::xo::db::apm_parameter instmixin parameter

  #
  # Methods on the parameter class object
  #
  parameter proc get_package_key_from_id {-package_id:required} {
    return [apm_package_key_from_id $package_id]
  }
  parameter proc get_package_id_from_package_key {-package_key:required} {
    return [ns_cache eval xotcl_package_cache package_id-$package_key {
      ::xo::dc get_value get_package_id {
        select package_id
        from apm_packages
        where package_key = :package_key
        fetch first 1 rows only
      }
    }]
  }

  parameter proc get_parameter_object {
                                       -parameter_name:required
                                       -package_id
                                       -package_key
                                       {-retry true}
                                     } {
    #::xo::PackageMgr instvar package_class
    if {![info exists package_key]} {
      set package_key [:get_package_key_from_id -package_id $package_id]
    }
    while {$package_key ne ""} {
      set key Parameter_id($package_key,$parameter_name)
      if {[info exists :$key]} {
        return [set :$key]
      }
      #
      # We did not find the parameter object for the current package
      # key. Loop up the parameter class (TODO: should be done from
      # object_type of package_id, but first, we have to store it
      # there).  We simply iterate here of the classes of packages
      # (only a few exist).
      #
      #:log "--p looking for $parameter_name in superclass of package_key=$package_key"
      set success 0
      set pkg_class [::xo::PackageMgr get_package_class_from_package_key $package_key]
      if {$pkg_class ne ""} {
        set sc [$pkg_class info superclass]
        if {[$sc exists package_key]} {
          set package_key [$sc package_key]
          set success 1
        }
      }
      if {!$success} break
    }
    if {$retry} {
      #
      # The parameter object was not found. Maybe this is a new
      # parameter, not known in this thread. We try to load it
      #
      set r [::xo::db::apm_parameter instantiate_objects \
                 -sql [::xo::db::apm_parameter instance_select_query \
                           -where_clause {
                             and parameter_name = :parameter_name
                             and package_key = :package_key
                           }] \
                 -object_class ::xo::db::apm_parameter \
                 -ignore_missing_package_ids true \
                 -as_ordered_composite false -named_objects true -destroy_on_cleanup false]
      #
      # Check for "retry" to avoid potential recursive loops
      #
      if {$r ne ""} {
        #
        # seems as if this parameter was newly defined
        #
        if {![info exists package_id]} {
          set package_id ""
        }
        return [:get_parameter_object \
                    -retry false \
                    -parameter_name $parameter_name \
                    -package_id $package_id \
                    -package_key $package_key]
      }
    }
    #
    # If everything fails, return empty.
    #
    return ""
  }

  parameter proc get_from_package_key {
                                       -package_key:required
                                       -parameter:required
                                       -default
                                     } {
    set parameter_obj [:get_parameter_object \
                           -package_key $package_key \
                           -parameter_name $parameter]
    if {$parameter_obj eq ""} {
      if {[info exists default]} {return $default}
      error "No parameter '$parameter' for package_key '$package_key' defined"
    }
    set package_id [:get_package_id_from_package_key -package_key $package_key]
    set value [$parameter_obj get -package_id $package_id]
    if {$value eq "" && [$parameter_obj set __success] == 0 && [info exists default]} {
      return $default
    } else {
      return $value
    }
  }

  parameter proc get {
                      -package_id
                      -parameter:required
                      -default
                      {-retry true}
                    } {
    if {![info exists package_id]} {
      #
      # Try to get the package id; if everything fails, use kernel_id
      # (to be compatible with traditional parameter::get)
      #
      set package_id [expr {[nsf::is object ::xo::cc] ?
                            [::xo::cc package_id] :
                            [ns_conn isconnected] ? [ad_conn package_id] : $::acs::kernel_id}]
    }
    ad_log_deprecated proc "xo::parameter get -parameter $parameter" parameter::get
    return [::parameter::get -parameter $parameter -package_id $package_id \
                {*}[expr {[info exists default] ? [list -default $default] : ""}]]

    set parameter_obj [:get_parameter_object \
                           -parameter_name $parameter \
                           -package_id $package_id \
                           -retry $retry]
    if {$parameter_obj ne ""} {
      set value [$parameter_obj get -package_id $package_id]
      if {$value eq "" && [$parameter_obj set __success] == 0} {
        return $default
      }
      return $value
    } else {
      return $default
    }
  }

  parameter proc set_value {
                      -package_id
                      -parameter:required
                      -value:required
                    } {

    if {![info exists package_id]} {
      #
      # Try to get the package id; if everything fails, use kernel_id
      # (to be compatible with traditional parameter::get)
      #
      set package_id [expr {[nsf::is object ::xo::cc] ?
                            [::xo::cc package_id] :
                            [ns_conn isconnected] ? [ad_conn package_id] : $::acs::kernel_id}]
    }

    ad_log_deprecated proc "xo::parameter set_value -parameter $parameter" parameter::set_value
    return [::parameter::set_value -package_id $package_id -parameter $parameter -value $value]

    set parameter_obj [:get_parameter_object -parameter_name $parameter -package_id $package_id]
    if {$parameter_obj ne ""} {
      $parameter_obj set_per_package_instance_value $package_id $value
    } else {
      error "could not create parameter object"
    }
  }

  #
  # Methods for parameter instances
  #
  if {[::acs::icanuse "nsv_dict"]} {
    #
    # Basic model (with nsv_dict):
    #
    #   nsv_dict CFG-X $package_id [list $parameter $value ...]
    #
    # The value X is just used for partitioning to avoid all
    # configuration values on a single mutex. This can be used for
    # fine-tuning mutex locks on such nsvs in the future.
    #
    parameter instproc -deprecated per_package_id_name {package_id} {
      xo::show_stack
      return CFG-[expr {$package_id % 2}]
    }
    parameter instproc -deprecated set_per_package_instance_value {package_id value} {
      set array [:per_package_id_name $package_id]
      ns_log notice "[list nsv_dict set $array $package_id ${:parameter_name} $value]"
      xo::show_stack
      nsv_dict set $array $package_id ${:parameter_name} $value
    }
    parameter instproc clear_per_package_instance_value {package_id} {
      set array [:per_package_id_name $package_id]
      if {[nsv_dict exists $array $package_id ${:parameter_name}]} {
        nsv_dict unset $array $package_id ${:parameter_name}
      }
    }
    parameter instproc get {-package_id:required} {
      set array [:per_package_id_name $package_id]
      #
      # Try to get the variable from the nsv. On success,
      #
      if {[nsv_dict get -varname result $array $package_id ${:parameter_name}]} {
        #:log "--parameter get <${:parameter_name}> for $package_id -> '$result'"
        set :__success 1
        return $result
      }
      # We could as well store per-package-key values,
      # but most probably, this is not needed if we use
      # the parameter default (which is per package-key).
      # With additional  per-package-key values, we could implement
      # a very simple "reset to default" for package-key values.
      #
      #     foreach cls $package_class_hierarchy {
      #       set nsv_array_name [:per_package_class_name $cls]
      #       if {[nsv_exists $nsv_array_name $key]} {
      #         #:log "--parameter get <$key> from $nsv_array_name --> '[nsv_get $nsv_array_name $key]'"
      #         return [nsv_get $nsv_array_name $key]
      #       }
      #     }
      #
      #:log "--parameter get <$key> from default of [:package_key] --> '[:default_value]'"
      set :__success 0
      return ${:default_value}
    }

  } else {
    #
    # Basic model (without nsv_dict):
    #
    #   ns_set CFG-$package_id $parameter $value
    #
    parameter instproc per_package_id_name {package_id} {
      return "CFG-$package_id"
    }
    parameter instproc set_per_package_instance_value {package_id value} {
      set array [:per_package_id_name $package_id]
      nsv_set $array [:parameter_name] $value
    }
    parameter instproc clear_per_package_instance_value {package_id} {
      set array [:per_package_id_name $package_id]
      if {[nsv_exists $array [:parameter_name]]} {
        nsv_unset $array [:parameter_name]
      }
    }
    #   parameter instproc per_package_class_name {package_class} {
    #     return "CFG-$package_class"
    #   }
    parameter instproc get {-package_id:required} {
      set key [:parameter_name]
      set nsv_array_name [:per_package_id_name $package_id]
      if {[nsv_exists $nsv_array_name $key]} {
        #:log "--parameter get <$key> from $nsv_array_name --> '[nsv_get $nsv_array_name $key]'"
        set :__success 1
        return [nsv_get $nsv_array_name $key]
      }
      # We could as well store per-package-key values,
      # but most probably, this is not needed if we use
      # the parameter default (which is per package-key).
      # With additional  per-package-key values, we could implement
      # a very simple "reset to default" for package-key values.
      #
      #     foreach cls $package_class_hierarchy {
      #       set nsv_array_name [:per_package_class_name $cls]
      #       if {[nsv_exists $nsv_array_name $key]} {
      #         #:log "--parameter get <$key> from $nsv_array_name --> '[nsv_get $nsv_array_name $key]'"
      #         return [nsv_get $nsv_array_name $key]
      #       }
      #     }
      #
      #:log "--parameter get <$key> from default of [:package_key] --> '[:default_value]'"
      set :__success 0
      return [:default_value]
    }

  }

  parameter instproc initialize_loaded_object {} {
    [self class] set Parameter_id(${:package_key},${:parameter_name}) [self]
  }


  # get apm_parameter objects
  ::xo::db::apm_parameter instantiate_objects \
      -sql [::xo::db::apm_parameter instance_select_query] \
      -object_class ::xo::db::apm_parameter \
      -ignore_missing_package_ids true \
      -as_ordered_composite false -named_objects true -destroy_on_cleanup false
  #  ns_log notice "--p got [llength [::xo::db::apm_parameter info instances]] parameters"
  #foreach p [::xo::db::apm_parameter info instances] { ns_log notice [$p serialize] }

  parameter proc initialize_parameters {} {
    #
    # Get those parameter values, which are different from the default
    # and remember these per package_id. For site-wide parameters -
    # which we do not handle here - the package_id is NULL, so we skip
    # it.
    #
    xo::dc foreach get_non_default_values {
      select p.parameter_id, p.package_key, v.package_id, p.parameter_name,
      p.default_value, v.attr_value
      from apm_parameters p, apm_parameter_values v
      where p.parameter_id = v.parameter_id
      and coalesce(attr_value,'') <> coalesce(p.default_value,'')
      and package_id is not null
    } {
      # ns_log notice "--p $parameter_id $package_key $package_id $parameter_name <$attr_value>"
      $parameter_id set_per_package_instance_value $package_id $attr_value
    }
  }

  #parameter initialize_parameters

  #
  # For the time being: catch changed parameter values
  #
  # ad_proc -public -callback subsite::parameter_changed -impl xotcl-param-procs {
  #   -package_id:required
  #   -parameter:required
  #   -value:required
  # } {
  #   Implementation of subsite::parameter_changed for xotcl param procs
  #
  #   @param package_id the package_id of the package the parameter was changed for
  #   @param parameter  the parameter name
  #   @param value      the new value
  # } {
  #   #
  #   # In order to use the existing interface for parameters, we catch
  #   # all parameter changes and update accordingly the values in the new
  #   # interface.
  #   #
  #   set package_key [apm_package_key_from_id $package_id]
  #   set parameter_obj [::xo::parameter get_parameter_object \
  #                          -package_key $package_key \
  #                          -parameter_name $parameter]
  #
  #   if {$parameter_obj eq ""} {
  #     # We have still no parameter. There must be something significantly wrong.
  #     ns_log warning "parameter $parameter for package $package_key, package_id $package_id does not exist (yet)"
  #   } else {
  #     $parameter_obj clear_per_package_instance_value $package_id
  #     if {[$parameter_obj default_value] ne $value} {
  #       $parameter_obj set_per_package_instance_value $package_id $value
  #     }
  #   }
  # }


  #
  #  A few test cases
  #
  #   ns_log notice "xotcl-request-monitor.max-url-stats=[parameter get_from_package_key \
      #       -package_key xotcl-request-monitor \
      #       -parameter max-url-stats]"

  #   set cmd1 "::parameter::get_from_package_key \
      #       -package_key xotcl-request-monitor \
      #       -parameter max-url-stats"
  #   set cmd2 "::xo::parameter get_from_package_key \
      #       -package_key xotcl-request-monitor \
      #       -parameter max-url-stats"
  #   ns_log notice "GET_PACKAGE_KEY old: [time $cmd1 100], new: [time $cmd2 100]"

  #   set pid 4906
  #   set pname trend-elements
  #   ns_log notice "xotcl-request-monitor.$pname=[parameter get \
      #       -package_id $pid -parameter $pname]"
  #   set cmd1 "::parameter::get -package_id $pid -parameter $pname"
  #   set cmd2 "::xo::parameter get -package_id $pid -parameter $pname"
  #   ns_log notice "GET old: [time $cmd1 100], new: [time $cmd2 100]"

  #
  #
  #
  #
  #   set p [parameter get_parameter_object -package_key xowiki -parameter_name dummy]
  #   ns_log notice "--p getobject => $p"
  #   if {$p eq ""} {
  #     set p [::xo::db::apm_parameter new_persistent_object  \
      #                -package_key "xowiki" \
      #                -parameter_name "dummy" \
      #                -default_value "testing" \
      #                -description "Description of test parameter" \
      #                -section_name ""]
  #     ns_log notice "--p created new parameter $p"
  #   }
  #   $p append default_value "1"
  #   $p save
  # $p delete

}

#
# Local variables:
#    mode: tcl
#    tcl-indent-level: 2
#    indent-tabs-mode: nil
# End: