• Publicity: Public Only All

acs-cache-procs.tcl

This file defines the following Objects and Classes: ::acs::HashKeyPartitionedCache[i], ::acs::KeyPartitionedCache[i], ::acs::PartitionedCache[i], ::acs::LockfreeCache[i], ::acs::Cache[i], ::acs::per_request_cache[i], ::acs::per_thread_cache[i], ::xo::xotcl_package_cache[i], ::xo::xotcl_object_cache[i], ::acs::site_nodes_cache[i], ::acs::site_nodes_children_cache[i], ::xowiki::cache[i], ::acs::permission_cache[i], ::xo::xotcl_object_type_cache[i], ::acs::group_cache[i], ::acs::misc_cache[i], ::acs::site_nodes_id_cache[i], ::acs::Cache[i], ::acs::Cache[i], ::acs::Cache[i], ::acs::Cache[i], ::acs::Cache[i], ::acs::Cache[i], ::acs::Cache[i], ::acs::Cache[i], ::acs::Cache[i], ::acs::Cache[i], ::acs::Cache[i], ::acs::HashKeyPartitionedCache[i], ::acs::HashKeyPartitionedCache[i], ::acs::HashKeyPartitionedCache[i], ::acs::KeyPartitionedCache[i], ::acs::KeyPartitionedCache[i], ::acs::LockfreeCache[i], ::acs::LockfreeCache[i], ::acs::LockfreeCache[i], ::acs::PartitionedCache[i], ::acs::PartitionedCache[i], ::acs::PartitionedCache[i], ::acs::PartitionedCache[i], ::acs::PartitionedCache[i], ::acs::KeyPartitionedCache[i]

Location:
packages/acs-tcl/tcl/acs-cache-procs.tcl

Procedures in this file

Detailed information

Class ::acs::Cache (public)

 ::nx::Class ::acs::Cache[i]

Base class for cache management

Testcases:
No testcase defined.

Class ::acs::HashKeyPartitionedCache (public)

 ::nx::Class ::acs::HashKeyPartitionedCache[i]

Partitioned cache, where the partition numbers are computed via a hash function. Key-partitioning is based on a modulo function using a special partition_key, which has to be numeric

Testcases:
No testcase defined.

Class ::acs::KeyPartitionedCache (public)

 ::nx::Class ::acs::KeyPartitionedCache[i]

Partitioned cache, where the partition numbers are computed via a modulo function from the numeric keys.

Testcases:
No testcase defined.

Class ::acs::LockfreeCache (public)

 ::nx::Class ::acs::LockfreeCache[i]

Lockfree caches are provided either as per-thread caches or per-request caches, sharing the property that accessing these values does not require locks. Typical applications of these caches are the per_request_cache and per_thread_cache.

See Also:

Testcases:
No testcase defined.

Class ::acs::PartitionedCache (public)

 ::nx::Class ::acs::PartitionedCache[i]

Partitioned cache infrastructure. Partition numbers are computed via a modulo function from the numeric keys.

Testcases:
No testcase defined.

Object ::acs::per_request_cache (public)

 ::acs::LockfreeCache ::acs::per_request_cache[i]

Lockfree cache with per-request live time of the entries. The purpose of the per-request cache is to cache computation results of a single request. The entries of this cache are therefore very short-lived. Some values are needed multiple times per request, and/or they should show consistently the same value during the same request, no matter, if concurrently, a value is changed (e.g. permissions). The per-request cache uses a Tcl variable in the global Tcl namespace, such it will be automatically reclaimed after the request. The per-request cache uses the prefix "::__acs_cache".

Testcases:
No testcase defined.

Object ::acs::per_thread_cache (public)

 ::acs::LockfreeCache ::acs::per_thread_cache[i]

Lockfree cache with per-thread live time of the entries. The per-thread caches use namespaced variables, which are not touched by the automatic per-request cleanup routines of the server. So, the values cached in one requests can be used by some later request in the same thread. The entries are kept in per-thread caches as long as the thread lives, there is so far no automatic mechanism to flush these. So, per-thread caches are typically used for values fetched from the database, which do not change, unless the server is restarted. Note: the usage of per-thread caches is only recommended for static values, which do no change during the life time of the server, since there is so far no automatic measure in place to the flush values in every thread.

Testcases:
No testcase defined.

Object ::xowiki::cache (public)

 ::acs::KeyPartitionedCache ::xowiki::cache[i]

Xowiki cache. This partitioned cache is used for various caching purposes for xowiki. It is used for example for caching includlets (when requested), or for full file names of xowiki::File objects. In case of bottlenecks, increase the number of partitions and the cache size.

Testcases:
No testcase defined.

acs::Cache method flush (public)

 <instance of acs::Cache[i]> flush [ -partition_key partition_key ] \
    key

Flush a single entry in the cache

Switches:
-partition_key (optional)
Parameters:
key (required)

Testcases:
No testcase defined.

acs::Cache method flush_all (public)

 <instance of acs::Cache[i]> flush_all

Flush all contents of all (partitioned) caches. In the case of a base ::acs::Cache, it is identical to "flush_cash".

Testcases:
No testcase defined.

acs::Cache method flush_cache (public)

 <instance of acs::Cache[i]> flush_cache \
    [ -partition_key partition_key ]

Flush all entries in a cache.

Switches:
-partition_key (optional)

Testcases:
No testcase defined.

acs::Cache method flush_pattern (public)

 <instance of acs::Cache[i]> flush_pattern \
    [ -partition_key partition_key ] pattern

Flush in the cache a value based on a pattern operation. Use this function rarely, since on large caches (e.g. 100k entries or more) the glob operation will cause long locks, which should be avoided. The partitioned variants can help to reduce the lock times.

Switches:
-partition_key (optional)
Parameters:
pattern (required)

Testcases:
No testcase defined.

acs::Cache method get (public)

 <instance of acs::Cache[i]> get [ -partition_key partition_key ] \
    key

The "get" method retrieves data from the cache. It should not be used for new applications due to likely race conditions, but legacy applications use this. As implementation, we use the AOLserver API emulation.

Switches:
-partition_key (optional)
Parameters:
key (required)

Testcases:
No testcase defined.

acs::Cache method init (public)

 <instance of acs::Cache[i]> init

If the name was not provided, use the object name as default.

Testcases:
No testcase defined.

acs::Cache method set (public)

 <instance of acs::Cache[i]> set [ -partition_key partition_key ] \
    key value

Set a single value in the cache. This code uses ns_cache_eval to achieve this behavior, which is typically an AOLserver idiom and should be avoided.

Switches:
-partition_key (optional)
Parameters:
key (required)
value (required)

Testcases:
No testcase defined.

acs::Cache method show_all (public)

 <instance of acs::Cache[i]> show_all

Log all cache keys to the system log. The primary usage is for debugging.

Testcases:
No testcase defined.

acs::HashKeyPartitionedCache method flush_pattern (public)

 <instance of acs::HashKeyPartitionedCache[i]> flush_pattern \
    -partition_key partition_key  pattern

flush just in all partitions

Switches:
-partition_key (required)
Parameters:
pattern (required)

Testcases:
No testcase defined.

acs::HashKeyPartitionedCache method set (public)

 <instance of acs::HashKeyPartitionedCache[i]> set \
    -partition_key partition_key  key value

Set a single value in the cache. It transforms the partition key into a hash value. This code uses ns_cache_eval to achieve this behavior, which is typically an AOLserver idiom and should be avoided.

Switches:
-partition_key (required)
Parameters:
key (required)
value (required)

Testcases:
No testcase defined.

acs::KeyPartitionedCache method flush_pattern (public)

 <instance of acs::KeyPartitionedCache[i]> flush_pattern \
    -partition_key partition_key  pattern

Flush just in the determined partition

Switches:
-partition_key (required, integer)
Parameters:
pattern (required)

Testcases:
No testcase defined.

acs::KeyPartitionedCache method set (public)

 <instance of acs::KeyPartitionedCache[i]> set \
    -partition_key partition_key  key value

Set a single value in the cache. This code uses ns_cache_eval to achieve this behavior, which is typically an AOLserver idiom and should be avoided.

Switches:
-partition_key (required, integer)
Parameters:
key (required)
value (required)

Testcases:
No testcase defined.

acs::LockfreeCache method eval (public)

 <instance of acs::LockfreeCache[i]> eval -key key  \
    [ -no_cache no_cache ] [ -no_empty ] \
    [ -from_cache_indicator from_cache_indicator ] cmd

Use the "prefix" to determine whether the cache is per-thread or per-request.

Switches:
-key (required)
key for caching, should start with package-key and a dot to avoid name clashes
-no_cache (optional)
list of returned values that should not be cached
-no_empty (optional, defaults to "false")
don't cache empty values. This flag is deprecated, one should use the no_cache flag instead.
-from_cache_indicator (optional)
variable name to indicate whether the returned value was from cache or not
Parameters:
cmd (required)
command to be executed.
Returns:
return the last value set (don't use "return").

Testcases:
No testcase defined.

acs::LockfreeCache method flush (public)

 <instance of acs::LockfreeCache[i]> flush [ -pattern pattern ]

Flush a cache entry based on the pattern (which might be wild-card-free). Currently, the clusterwide flushing is omitted. We have the per-request cache (clusterwide operations do not make sense for this) and per-thread caching. The per-thread caching application have to be aware that flushing is happening only in one thread, so clusterwide operations will only start to make sense, when the all threads of a server would be cleaned.

Switches:
-pattern (optional, defaults to "*")

Testcases:
No testcase defined.

acs::LockfreeCache method get (public)

 <instance of acs::LockfreeCache[i]> get -key key  var

Get entry with the provided key from this cache if it exists. In most cases, the "eval" method should be used.

Switches:
-key (required)
cache key
Parameters:
var (required)
Returns:
return boolean value indicating success.

Testcases:
No testcase defined.

acs::PartitionedCache method flush_all (public)

 <instance of acs::PartitionedCache[i]> flush_all \
    [ -partition_key partition_key ]

Flush all entries in all partitions of a cache.

Switches:
-partition_key (optional)

Testcases:
No testcase defined.

acs::PartitionedCache method init (public)

 <instance of acs::PartitionedCache[i]> init

If the name was not provided, use the object name as default for the cache name.

Testcases:
No testcase defined.

acs::PartitionedCache method show_all (public)

 <instance of acs::PartitionedCache[i]> show_all

Log all cache keys of all partitions to the system log. The primary usage is for debugging.

Testcases:
No testcase defined.

acs::disk_cache_eval (public)

 acs::disk_cache_eval -call call -key key -id id

Evaluate an expression. When the acs-tcl.DiskCache parameter is set, cache the result on the disk. If a cache already exists, return the cached value.

Switches:
-call (required)
a Tcl snippet executed in the caller scope.
-key (required)
a key used to name the directory where the disk cache will be stored.
-id (required)
an id used to name the file where the disk cache will be stored. The name will also depend on a hash of the actual snippet.

Partial Call Graph (max 5 caller/called nodes):
%3 test_disk_cache disk_cache (test acs-tcl) acs::disk_cache_eval acs::disk_cache_eval test_disk_cache->acs::disk_cache_eval ad_file ad_file (public) acs::disk_cache_eval->ad_file ad_tmpdir ad_tmpdir (public) acs::disk_cache_eval->ad_tmpdir parameter::get_from_package_key parameter::get_from_package_key (public) acs::disk_cache_eval->parameter::get_from_package_key template::util::read_file template::util::read_file (public) acs::disk_cache_eval->template::util::read_file template::util::write_file template::util::write_file (public) acs::disk_cache_eval->template::util::write_file

Testcases:
disk_cache

acs::disk_cache_flush (public)

 acs::disk_cache_flush -key key -id id

Flushes the filesystem cache.

Switches:
-key (required)
the key used to name the directory where the disk cache is stored.
-id (required)
the id used to name the file where the disk cache is stored.
See Also:

Partial Call Graph (max 5 caller/called nodes):
%3 test_disk_cache disk_cache (test acs-tcl) acs::disk_cache_flush acs::disk_cache_flush test_disk_cache->acs::disk_cache_flush ad_tmpdir ad_tmpdir (public) acs::disk_cache_flush->ad_tmpdir

Testcases:
disk_cache
[ hide source ] | [ make this the default ]

Content File Source

#
#    Copyright (C) 2018 Gustaf Neumann, neumann@wu-wien.ac.at
#
#       Vienna University of Economics and Business
#       Institute of Information Systems and New Media
#       A-1020, Welthandelsplatz 1
#       Vienna, Austria
#
#    This is a BSD-Style license applicable for this file.
#
#    Permission to use, copy, modify, distribute, and sell this
#    software and its documentation for any purpose is hereby granted
#    without fee, provided that the above copyright notice appears in
#    all copies and that both that copyright notice and this permission
#    notice appear in supporting documentation. We make no
#    representations about the suitability of this software for any
#    purpose.  It is provided "as is" without express or implied
#    warranty.
#

namespace eval ::acs {

    ##########################################################################
    #
    # Generic Cache class
    #
    ##########################################################################

    nx::Class create ::acs::Cache {
        #
        # Base class for cache management
        #
        :property name
        :property parameter:required
        :property package_key:required
        :property maxentry:integer
        :property {timeout 5m}
        :property {default_size 100KB}

        :method cache_name {key} {
            #
            # More or less dummy function, which can be refined.  The
            # base definition completely ignores "key".
            #
            return ${:name}
        }

        :method get_size {} {
            #
            # Determine the cache size depending on configuration
            # variables.
            #
            set specifiedSize [::parameter::get_from_package_key \
                                   -package_key ${:package_key} \
                                   -parameter "${:parameter}Size" \
                                   -default ${:default_size}]
            if {[::nsf::is integer $specifiedSize]} {
                set size $specifiedSize
            } else {
                set size [ns_baseunit -size $specifiedSize]
            }
            return $size
        }

        :public method flush {{-partition_key} key} {
            #
            # Flush a single entry in the cache
            #
            if {![info exists partition_key]} {
                set partition_key $key
            }
            ::acs::clusterwide ns_cache flush [:cache_name $partition_key$key
        }

        :public method eval {{-partition_key} {-expires} {-timeout} {-per_request:switch} key command} {
            #
            # Evaluate the command unless the result was already computed before and cached.
            #
            # @param expires Lifetime of the cache entry.
            #        The entry will be purged automatically when the time is reached. The time is in seconds
            #        unless a time unit is specified (e.g., 5m)
            # @param timeout Maximum time to wait for the command to complete. The time is in seconds
            #        unless a time unit is specified (e.g., 2.5m)
            # @param partition_key Used for determining the cache
            #        name in partitioned caches. The partition key is computed typically
            #        automatically depending on the cache type.
            # @param per_request When set, cache the result per
            #        request. So far, no attempt is made to flush
            #        the result during the lifetime of the request.
            # @param key The cache key
            # @param command The command to be executed when the result is not yet cached.
            #
            if {![info exists partition_key]} {
                set partition_key $key
            }
            foreach optional_parameter {expires timeout} {
                if {[info exists $optional_parameter]} {
                    set ${optional_parameter}_flag [list -$optional_parameter [set $optional_parameter]]
                } else {
                    set ${optional_parameter}_flag ""
                }
            }
            set cache_name [:cache_name $partition_key]
            try {
                if {$per_request} {
                    acs::per_request_cache eval -key ::acs-${cache_name}($key) {
                        :uplevel [list ns_cache_eval \
                                      {*}$expires_flag {*}$timeout_flag -- \
                                      $cache_name $key $command]
                    }
                } else {
                    :uplevel [list ns_cache_eval {*}$expires_flag {*}$timeout_flag -- \
                                  $cache_name $key $command]
                }

            } on break {r} {
                #
                # When the command ends with "break", it means:
                # "don't cache". We return in this case always a
                # 0.
                #
                #ns_log notice "====================== [self] $key -> break -> <$r>"
                return 0

            } on ok {r} {
                return $r
            }
        }

        :public method set {-partition_key key value} {
            #
            # Set a single value in the cache. This code uses
            # ns_cache_eval to achieve this behavior, which is
            # typically an AOLserver idiom and should be avoided.
            #
            if {![info exists partition_key]} {
                set partition_key $key
            }
            :uplevel [list ns_cache_eval -force -- [:cache_name $partition_key$key [list set _ $value]]
        }

        :public method flush_pattern {{-partition_key ""} pattern} {
            #
            # Flush in the cache a value based on a pattern
            # operation. Use this function rarely, since on large
            # caches (e.g. 100k entries or more) the glob
            # operation will cause long locks, which should be
            # avoided. The partitioned variants can help to reduce
            # the lock times.
            #
            return [::acs::clusterwide ns_cache_flush -glob [:cache_name $partition_key$pattern]
        }

        :method cache_create {name size} {
            #
            # Create a cache.
            #
            ns_cache_create \
                -timeout ${:timeout} \
                {*}[expr {[info exists :maxentry] ? "-maxentry ${:maxentry}" : ""}] \
                $name $size
        }

        :public method get {-partition_key key} {
            #
            # The "get" method retrieves data from the cache. It
            # should not be used for new applications due to likely
            # race conditions, but legacy applications use this.  As
            # implementation, we use the AOLserver API emulation.
            #
            if {![info exists partition_key]} {
                set partition_key $key
            }
            return [ns_cache get [:cache_name $partition_key$key]
        }

        :public method show_all {} {
            #
            # Log all cache keys to the system log. The primary usage
            # is for debugging.
            #
            ns_log notice "content of ${:name}: [ns_cache_keys ${:name}]"
        }

        :public method flush_cache {{-partition_key ""}} {
            #
            # Flush all entries in a cache.
            #
            ::acs::clusterwide ns_cache_flush [:cache_name $partition_key]
            #ns_log notice "flush_all -> ns_cache_flush [:cache_name $partition_key]"
            #ns_log notice "... content of ${:name}: [ns_cache_keys ${:name}]"
        }

        :public method flush_all {} {
            #
            # Flush all contents of all (partitioned) caches. In the
            # case of a base ::acs::Cache, it is identical to
            # "flush_cash".
            #
            :flush_cache
        }

        :public method init {} {
            #
            # If the name was not provided, use the object name as
            # default.
            #
            if {![info exists :name]} {
                set :name [namespace tail [current]]
            }
            :cache_create ${:name} [:get_size]
        }
    }

    ##########################################################################
    #
    # Simple Partitioned Cache class
    #
    # Partitioning is based on a modulo function using the cache
    # key, which has to be numeric.
    #
    ##########################################################################

    nx::Class create ::acs::PartitionedCache -superclasses ::acs::Cache {
        #
        # Partitioned cache infrastructure. Partition numbers are
        # computed via a modulo function from the numeric keys.
        #

        :property {partitions:integer 1}

        :protected method cache_name {key:integer} {
            #
            # Return the cache_name always as the same Tcl_Obj (list
            # element) rather than concatenating always a fresh
            # Tcl_Obj dynamically the fly (type string). Caching the
            # cache structure in the dynamic Tcl_Obj can't not work.
            #
            return [lindex ${:partition_names} [expr {$key % ${:partitions}}]]
        }

        :public method init {} {
            #
            # If the name was not provided, use the object name as
            # default for the cache name.
            #
            if {![info exists :name]} {
                set :name [namespace tail [current]]
            }
            set :partitions [::parameter::get_from_package_key \
                                -package_key ${:package_key} \
                                -parameter "${:parameter}Partitions" \
                                -default ${:partitions}]
            #
            # Create multiple separate caches depending on the
            # partitions. A PartitionedCache requires to have a
            # partitioning function that determines the nth partition
            # number from some partition_key.
            #
            set size [expr {[:get_size] / ${:partitions}}]
            set :partition_names {}
            for {set i 0} {$i < ${:partitions}} {incr i} {
                lappend :partition_names ${:name}-$i
                :cache_create ${:name}-$i $size
            }
        }

        :public method flush_all {{-partition_key ""}} {
            #
            # Flush all entries in all partitions of a cache.
            #
            for {set i 0} {$i < ${:partitions}} {incr i} {
                ::acs::clusterwide ns_cache_flush ${:name}-$i
                #ns_log notice "flush_all: ns_cache_flush ${:name}-$i"
                #ns_log notice "... content of ${:name}-$i: [ns_cache_keys ${:name}-$i]"
            }
        }

        :method flush_pattern_in_all_partitions {pattern} {
            #
            # Flush matching entries in all partitions of a cache based on
            # a pattern.
            #
            for {set i 0} {$i < ${:partitions}} {incr i} {
                ::acs::clusterwide ns_cache_flush -glob ${:name}-$i $pattern
                ns_log notice "flush_pattern_in_all_partitions: ns_cache_flush ${:name}-$i $pattern"
                #ns_log notice "... content of ${:name}-$i: [ns_cache_keys ${:name}-$i]"
            }
        }

        :public method show_all {} {
            #
            # Log all cache keys of all partitions to the system
            # log. The primary usage is for debugging.
            #
            for {set i 0} {$i < ${:partitions}} {incr i} {
                ns_log notice "content of ${:name}-$i: [ns_cache_keys ${:name}-$i]"
            }

        }

    }

    ##########################################################################
    #
    # Class for key-partitioned caches
    #
    ##########################################################################
    nx::Class create ::acs::KeyPartitionedCache -superclasses ::acs::PartitionedCache {
        #
        # Partitioned cache, where the partition numbers are computed
        # via a modulo function from the numeric keys.
        #

        :property {partitions:integer 1}

        :public method flush_pattern {{-partition_key:integer,required} pattern} {
            #
            # Flush just in the determined partition
            #
            next
        }

        #:public method flush {{-partition_key:integer,required} key} {
        #    next
        #}

        :public method set {{-partition_key:integer,required} key value} {
            #
            # Set a single value in the cache. This code uses
            # ns_cache_eval to achieve this behavior, which is
            # typically an AOLserver idiom and should be avoided.
            #
            next
        }
    }

    ##########################################################################
    #
    # Class for hash-key-partitioned caches
    #
    ##########################################################################
    nx::Class create ::acs::HashKeyPartitionedCache -superclasses ::acs::KeyPartitionedCache {
        #
        # Partitioned cache, where the partition numbers are computed
        # via a hash function.
        #
        # Key-partitioning is based on a modulo function using a special
        # partition_key, which has to be numeric
        #

        :property {partitions:integer 2}

        :public method flush_pattern {{-partition_key:required} pattern} {
            #
            # flush just in all partitions
            #
            :flush_pattern_in_all_partitions $pattern
        }

        :public method set {{-partition_key:required} key value} {
            #
            # Set a single value in the cache. It transforms the
            # partition key into a hash value. This code uses
            # ns_cache_eval to achieve this behavior, which is
            # typically an AOLserver idiom and should be avoided.
            #
            next [list -partition_key [ns_hash $partition_key$pattern]
        }

        :protected method cache_name {key} {
            next [list [ns_hash $key]]
        }

    }
}

namespace eval ::acs {
    ##########################################################################
    #
    # ::acs::LockfreeCache: Per-thread and per-request Cache
    #
    ##########################################################################
    nx::Class create ::acs::LockfreeCache {
        #
        # Lockfree caches are provided either as per-thread caches or
        # per-request caches, sharing the property that accessing these
        # values does not require locks.
        #
        # Typical applications of these caches are the per_request_cache and per_thread_cache.
        #
        # @see Object ::acs::per_request_cache
        # @see Object ::acs::per_thread_cache
        #
        :property {prefix}

        :public method get {
            {-key:required}
            var
        } {
            #
            # Get entry with the provided key from this cache if it
            # exists. In most cases, the "eval" method should be used.
            #
            # @param key cache key
            # @return return boolean value indicating success.
            #
            if {[info exists ${:prefix}] && [dict exists [set ${:prefix}$key]} {
                :upvar $var value
                set value [dict get [set ${:prefix}$key]
                return 1
            }
            return 0
        }

        :public method eval {
            {-key:required}
            {-no_cache}
            {-no_empty:switch false}
            {-from_cache_indicator}
            cmd
        } {
            #
            # Use the "prefix" to determine whether the cache is
            # per-thread or per-request.
            #
            # @param key key for caching, should start with package-key
            #            and a dot to avoid name clashes
            # @param cmd command to be executed.
            # @param no_empty don't cache empty values. This flag is
            #        deprecated, one should use the no_cache flag
            #        instead.
            # @param no_cache list of returned values that should not be cached
            # @param from_cache_indicator variable name to indicate whether
            #        the returned value was from cache or not
            #
            # @return return the last value set (don't use "return").
            #
            if {[info exists from_cache_indicator]} {
                :upvar $from_cache_indicator from_cache
            }

            #if {![info exists ${:prefix}]} {
            #    ns_log notice "### exists ${:prefix} ==> 0"
            #} else {
            #    ns_log notice "### [list dict exists [set ${:prefix}] $key] ==>  [dict exists [set ${:prefix}] $key]"
            #}

            if {![info exists ${:prefix}] || ![dict exists [set ${:prefix}$key]} {
                #ns_log notice "### call cmd <$cmd>"
                set from_cache 0
                set value [:uplevel $cmd]
                if {$no_empty} {
                    ad_log warning "no_empty flag is deprecated and will be dropped in the future."
                    lappend no_cache ""
                }
                if {[info exists no_cache] && $value in $no_cache} {
                    #ns_log notice "### cache eval $key returns <$value> without caching"
                    return $value
                }
                #if {$value eq "0"} {
                #    ns_log notice "### cache eval $key returns <$value> with caching"
                #}
                dict set ${:prefix} $key $value
                #ns_log notice "### [list dict set ${:prefix} $key $value]"
            } else {
                set from_cache 1
                set value [dict get [set ${:prefix}$key]
            }
            #ns_log notice "### will return [list dict get ${:prefix} $key]"
            return $value
        }

        #:public method flush {
        #   {-pattern *}
        #} {
        #    #
        #    # Flush a cache entry based on the pattern (which might be
        #    # wild-card-free).
        #    #
        #    ::acs::clusterwide [self] flush_local -pattern $pattern
        #}

        :public method flush {
           {-pattern *}
        } {
            #
            # Flush a cache entry based on the pattern (which might be
            # wild-card-free). Currently, the clusterwide flushing is
            # omitted.
            #
            # We have the per-request cache (clusterwide operations do
            # not make sense for this) and per-thread caching. The
            # per-thread caching application have to be aware that
            # flushing is happening only in one thread, so clusterwide
            # operations will only start to make sense, when the all
            # threads of a server would be cleaned.
            #
            if {[info exists ${:prefix}]} {
                if {$pattern eq "*"} {
                    #ns_log notice "### dict flush ${:prefix} <$pattern>"
                    unset -nocomplain ${:prefix}
                } elseif {[string first "*" $pattern] != -1} {
                    #
                    # A real pattern with wild-card was provided.
                    #
                    set keys [dict keys [set ${:prefix}$pattern]
                    #ns_log notice "### dict flush ${:prefix} <$pattern> -> [llength $keys]"
                    foreach key $keys {
                        dict unset ${:prefix} $key
                    }
                } elseif [dict exists [set ${:prefix}$pattern] {
                    #
                    # A "pattern" without a wildcard was provided
                    #
                    dict unset ${:prefix} $pattern
                }
            }
        }

        :create per_request_cache -prefix ::__acs_cache {
            #
            # Lockfree cache with per-request live time of the
            # entries.
            #
            # The purpose of the per-request cache is to cache
            # computation results of a single request.  The entries of
            # this cache are therefore very short-lived. Some values
            # are needed multiple times per request, and/or they
            # should show consistently the same value during the same
            # request, no matter, if concurrently, a value is changed
            # (e.g. permissions).
            #
            # The per-request cache uses a Tcl variable in the global
            # Tcl namespace, such it will be automatically reclaimed
            # after the request. The per-request cache uses the prefix
            # "::__acs_cache".
            #
        }

        #
        # Define the "per_thread_cache"
        #
        set docString {
            #
            # Lockfree cache with per-thread live time of the entries.
            #
            # The per-thread caches use namespaced variables,
            # which are not touched by the automatic per-request
            # cleanup routines of the server. So, the values
            # cached in one requests can be used by some later
            # request in the same thread. The entries are kept in
            # per-thread caches as long as the thread lives, there
            # is so far no automatic mechanism to flush these. So,
            # per-thread caches are typically used for values
            # fetched from the database, which do not change,
            # unless the server is restarted.
            #
            # Note: the usage of per-thread caches is only
            # recommended for static values, which do no change
            # during the life time of the server, since there is
            # so far no automatic measure in place to the flush
            # values in every thread.
        }
        if {[ns_config "ns/parameters" cachingmode "per-node"] eq "none"} {
            #
            # If caching mode is "none", let the "per_thread_cache" behave
            # like the "per_request_cache".
            #
            :create per_thread_cache -prefix ::__acs_cache $docString
            ns_log notice "cachingmode [ns_config "ns/parameters" cachingmode singlenode]" \
                "-> per_thread_cache behaves like per-request_cache"

        } else {
            #
            # The per-thread cache uses namespaced Tcl variables, identified
            # by the prefix "::acs:cache"
            #
            :create per_thread_cache -prefix ::acs::cache $docString
        }
    }
    namespace eval ::acs::cache {}
}

namespace eval ::acs {
    ad_proc -private try_cache {cache operation args} {

        Function to support caching during bootstrap.  When the
        provided cache exists, then use it for caching, otherwise
        perform uncalled call. This function is made intentionally
        private, since this should only be required during
        bootstrapping. It does not make sense to wrap arbitrary caching
        calls with this function.

    } {
        if {
            [namespace which $cache] ne "" &&
            [$cache info lookup methods $operation] ne ""
        } {
            return [uplevel 1 [list $cache $operation {*}$args]]
        } else {
            #
            # Complain only, when
            # a) not during initial install, and
            # b) if this is not during startup of an installed version
            #
            set complain_p [expr {[ns_ictl epoch] > 0 && [nsv_names acs_installer] eq ""}]
            if {$operation eq "eval"} {
                nsf::parseargs {{-partition_key} {-expires} {-per_request:switch} key command} $args
                if {$complain_p} {
                    ns_log warning "no cache $cache: need direct call $key $args"
                }
                #ns_log warning "no cache $cache: need direct call $key [info exists partition_key] <$command>"
                return [uplevel 1 $command]
            }
            if {$complain_p} {
                ns_log warning "no cache $cache: call ignored"
            }
        }
    }
}

namespace eval ::acs {
    #
    # Experimental disk-cache, to test whether this can speed up long
    # calls, producing potentially large output ..
    #
    # The interface should be probably streamlined with the other
    # chaching infrastructure.
    #
    # Documentation follows.

    if { [apm_first_time_loading_p] } {
        nsv_set ad_disk_cache mutex [ns_mutex create disk_cache]
    }

    d_proc -public disk_cache_flush {
        -key:required
        -id:required
    } {
        Flushes the filesystem cache.

        @param key the key used to name the directory where the disk cache
               is stored.
        @param id the id used to name the file where the disk cache is
               stored.

        @see acs::disk_cache_eval
    } {
        set dir [ad_tmpdir]/oacs-cache/$key
        foreach file [glob -nocomplain $dir/$id-*] {
            file delete -- $file
            ns_log notice "FLUSH file delete -- $file"
        }
    }

    d_proc -public disk_cache_eval {
        -call:required
        -key:required
        -id:required
    } {
        Evaluate an expression. When the acs-tcl.DiskCache parameter is
        set, cache the result on the disk. If a cache already exists,
        return the cached value.

        @param call a Tcl snippet executed in the caller scope.
        @param key a key used to name the directory where the disk cache
               will be stored.
        @param id an id used to name the file where the disk cache will be
              stored. The name will also depend on a hash of the
              actual snippet.
    } {
        set cache [::parameter::get_from_package_key \
                       -package_key acs-tcl \
                       -parameter DiskCache \
                       -default 1]
        if {$cache} {
            set hash [ns_sha1 $call]
            set dir [ad_tmpdir]/oacs-cache/$key
            set file_name $dir/$id-$hash
            if {![ad_file isdirectory $dir]} {
                file mkdir $dir
            }
            ns_mutex eval [nsv_get ad_disk_cache mutex] {
                if {[ad_file readable $file_name]} {
                    set result [template::util::read_file $file_name]
                } else {
                    set result [uplevel $call]
                    template::util::write_file $file_name $result
                }
            }
        } else {
            set result [uplevel $call]
        }
        return $result
    }
}


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