Forum OpenACS Development: connection-global cache: namespace problem?

Hi,

I've run into a little problem with the snippet of code below. It is not working in the sense it won't cache values in the global namespace as intended. Please help. (Relevant parts of the code is highlighted in bold)

I call (a variant of) this proc from my scripts like this:

set name [curriculum::conn name] and expect it to return the value from a query (or whatever) the first time I call it, and that it returns a cached value from the global variable cu_conn(name) on the following calls.

It turns out, however, that it always returns the uncached name; "Name from an expensive query, say - NOT CACHED" ...

This technique has more or less (with emphasis on more) been lifted from bug_tracker::conn and, of course, from ad_conn, so, if what I've found is an actual bug and not just a result of my working to hard on a saturday evening 😊, it _may_ also be a problem in Bug Tracker (Probably not with ad_conn since it doesn'n utilize namespaces). I'm not saying Bug Tracker suffers from this, but it could explain the (ahem) relative sluggishness of the bug tracker here on openacs.org 😉.

namespace eval curriculum {

    ad_proc -public conn {
        args
    } {
        global cu_conn

        set flag [lindex $args 0]
        if { [string index $flag 0] != "-" } {
            set var $flag
            set flag "-get"
        } else {
            set var [lindex $args 1]
        }

        switch -- $flag {
            -set {
                set cu_conn($var) [lindex $args 2]
            }

            -get {
                if { [info exists cu_conn($var)] } {
                    return "WOO HOO! $cu_conn($var) - CACHED"
		}
                switch -- $var {
                    name {
                        set cu_conn($var) "Name from an expensive query, say"

                        return "$cu_conn($var) - NOT CACHED"
                    }
                    default {
                        error "Unknown variable $var"
                    }
                }
            }

            default {
                error "cu_conn: unknown flag $flag"
            }
        }
    }



}

Collapse
Posted by Ola Hansson on
When I put this in my script;
global cu_conn

doc_return 200 text/plain $cu_conn(name)
... it happily outputs the message "Name from an expensive query, say".

The "info exists" part does not work, it appears.

Collapse
Posted by David Walker on
This kind of variable caching only works for variables that are filled at startup and never changes.  Once the server is running it has a number of distinct interpreters that do not share global variables.

Try to use "nsv_set collection_name var_name var_value" and "nsv_get collection_name var_name" instead.

Collapse
Posted by Roberto Mello on
This has nothing to do with your problem, but it will help you in debugging and will improve readability of your code.

I noticed that much of the namespaced code coming in to OpenACS is following the style you use:

namespace eval foo {
    ad_proc -public bar {
    }
}

This looks innocent, but there are several problems with doing things this way.

It makes it harder to read, because you have to keep all your procs in one big block. When reading through some code in some packages that have many procs declared that way, I end up having to scroll back all the time to know in which proc or namespace I am in.

Also it's hard to make sure you're in the right level of indentation, and it unnecessarily pushes one more level of indentation in.

There is what I think is a better way. acs-templating uses the following style, and I think it's a much better style to use accross OpenACS.

namespace eval foo {}

ad_proc -public ::foo::bar {
}

This way you can clearly see that "bar" belongs to the "foo" namespace. Your proc won't seem endless, because you're at the first level of indentation.

Overall I think it's much more readable and maintanable.

-Roberto

Collapse
Posted by Bart Teeuwisse on
Another important benefit from Roberto's method is that it allows Emacs to index the proc ::foo::bar with the fully qualified name rather than bar.

When bar is called outside the namespace foo the procedure is called with the fully qualified name ::foo::bar. Looking up the proc's definition with Emacs's tag search (M-.) only works with Roberto's method.

/Bart

Collapse
Posted by Don Baccus on
Yes, I prefer Roberto's/acs-templating's style too.

As far as the problem at hand ... David's right.  The "ad_conn" code your looking at caches information for a single HTTP request.  The code's called by a filter invoked for each HTTP request and caches information related to that request.  As David mentions GLOBALs are local to each interpreter, so they're appropriate for caching per-connection information.

You're trying to cache information across interpreters and, again as David mentions, nsv_* will do this.  You could also look into using ns_cache (check util_memoize).

Collapse
Posted by Ola Hansson on
Thanks David,

I will try something like the following and see how it works (it seems to work fairly well so far) ...


ad_proc -public ::curriculum::conn {
    args
} {
    set flag [lindex $args 0]
    if { [string index $flag 0] != "-" } {
        set var $flag
        set flag "-get"
    } else {
        set var [lindex $args 1]
    }

    set my_array MyArray

    switch -- $flag {
        -set {
            # Set cache key $var to some value and return it
            return [nsv_set $my_array $var [lindex $args 2]]
        }
        -flush {
            # Unset key $var - effectively flushes the cache
            nsv_unset $my_array $var
        }
        -nocache {
            # Unset key $var - effectively flushes the cache
            nsv_unset $my_array $var

            # Call ourself with the -get flag and return fresh data
            ::curriculum::conn -get $var
        }
        -get {
            # Get value of key $var - from query or cache
            if { [nsv_exists $my_array $var] } {
                return "[nsv_get $my_array $var] - CACHED"
            }
            switch -- $var {
                name -
                shoesize -
		weight -
                whatever {
                    return "[nsv_set $my_array $var "$var from an expensive query, say"] - NOT CACHED"
		}
                default {
                    error "Unknown variable $var"
                }
            }
        }
        default {
            error "::curriculum::conn: unknown flag $flag"
        }
    }
}

Roberto: That's a couple of really good points, I think. We should stick to this advice unless we come up with something better still.

Getting rid of an unnessecary level of indentation is worth gold!

Don: I have checked out ns_cache two weeks ago and a thread-private cache does not work since there are ~5 threads in each connection, I believe. The Global ns_cache is accessible to all threads, which is not what I want (I guess).

I want to cache across interpreters but not _all_ interpreters - only the five or so per connection, no?

I hope there is a smooth way to make caches accessible on a per user basis.

I feel as though I've probably misunderstood some of the interpreter/thread/connection mess (nothing the community couldn't help straighten out, I'm sure) and it's too late in the evening now, anyway, so I'll have to look at this and eventual extra answers in the morning...

/Ola

Collapse
Posted by Ola Hansson on
Okay. I think I am beginning to get this:

one HTTP request -> one connection -> one thread -> one interpreter ...

It now looks like what I should use to cache "per user" information is many global ns_caches (or nsv arrays); at least one per user who needs to have data cached (I could just suffix the collection name with $user_id to distinguish them), but it might make sense to split it up further based on what kind of data it is.

Is it better to have just a few big collections than to have many less big ones?

Global nscaches and nsv arrays seem to solve the exact same thing although ns_cache is probably a little faster since it's just a tcl API on top of a C module. AFAICT, ns_cache also provides better transaction control with less hassle.

Which method would folks recommend, and would you mind stating your reasons for your choice?

PS. Sorry Don. I didn't mean for it to sound like I doubted your suggestion of ns_cache...

Collapse
Posted by Tilmann Singer on
If you want to cache something per session (user) you might also want to look into ad_set_client_property and ad_get_client_property, with '-persistent f' set.
Collapse
Posted by Ola Hansson on
I studied ad_*_client_property and it looks like it could be useful. I had kind of overlooked it, so thanks for mentioning it.

Here is the current version of the "cache exchange" proc in case you want to steal it or comment on it. Serious testing needs to be performed before I trust it, though.

namespace eval curriculum {}

ad_proc -public ::curriculum::conn {
    args
} {
    set flag [lindex $args 0]
    if { [string index $flag 0] != "-" } {
        set var $flag
        set flag "-get"
    } else {
        set var [lindex $args 1]
    }

    set module MyModule

    switch -- $flag {
        -set {
            # Set cache key $var of module $module to some value and return it
            set value [lindex $args 2]
            ad_set_client_property -persistent f $module $var $value
            return $value
        }
        -flush {
            # Flush the cache for key $var in module $module
            util_memoize_flush [list sec_lookup_property \
                                    [ad_conn session_id] $module $var]
        }
        -nocache {
            # Call ourself with the -flush flag to flush the cache
            ::curriculum::conn -flush $var

            # Call ourself with the -get flag and return fresh data
            ::curriculum::conn -get $var
        }
        -get {
            # Get value of key $var in module $module - from query or cache
            switch -- $var {
                name -
                shoesize -
                weight -
                whatever {
                    set value [ad_get_client_property \
                                      -cache_only t -default "" $module $var]

                    if { ![empty_string_p $value] } {
                        return $value
                    }

                    set info "Result from query"

                    return [::curriculum::conn -set $var $info]
                }
                default {
                    error "Unknown variable $var"
                }
            }
        }
        default {
            error "::curriculum::conn: unknown flag $flag"
        }
    }
}