Documentation ¶
Overview ¶
Package cached provides a cache mechanism based on etags to lazily build, and/or cache results from expensive operation such that those operations are not repeated unnecessarily. The operations can be created as a tree, and replaced dynamically as needed.
All the operations in this module are thread-safe.
Dependencies and types of caches ¶
This package uses a source/transform/sink model of caches to build the dependency tree, and can be used as follows:
- Func: A source cache that recomputes the content every time.
- Once: A source cache that always produces the same content, it is only called once.
- Transform: A cache that transforms data from one format to another. It's only refreshed when the source changes.
- Merge: A cache that aggregates multiple caches in a map into one. It's only refreshed when the source changes.
- MergeList: A cache that aggregates multiple caches in a list into one. It's only refreshed when the source changes.
- Atomic: A cache adapter that atomically replaces the source with a new one.
- LastSuccess: A cache adapter that caches the last successful and returns it if the next call fails. It extends Atomic.
Etags ¶
Etags in this library is a cache version identifier. It doesn't necessarily strictly match to the semantics of http `etags`, but are somewhat inspired from it and function with the same principles. Hashing the content is a good way to guarantee that your function is never going to be called spuriously. In Kubernetes world, this could be a `resourceVersion`, this can be an actual etag, a hash, a UUID (if the cache always changes), or even a made-up string when the content of the cache never changes.
Example ¶
Here is an example of how one can write a cache that will constantly be pulled, while actually recomputing the results only as needed.
package main import ( "bytes" "encoding/json" "errors" "fmt" "k8s.io/kube-openapi/pkg/cached" ) func main() { // Merge Json is a replaceable cache, since we'll want it to // change a few times. mergeJson := &cached.LastSuccess[[]byte]{} one := cached.Once(cached.Func(func() ([]byte, string, error) { // This one is computed lazily, only when requested, and only once. return []byte("one"), "one", nil })) two := cached.Func(func() ([]byte, string, error) { // This cache is re-computed every time. return []byte("two"), "two", nil }) // This cache is computed once, and is not lazy at all. three := cached.Static([]byte("three"), "three") // This cache will allow us to replace a branch of the tree // efficiently. lastSuccess := &cached.LastSuccess[[]byte]{} lastSuccess.Store(cached.Static([]byte("four"), "four")) merger := func(results map[string]cached.Result[[]byte]) ([]byte, string, error) { var out = []json.RawMessage{} var resultEtag string for _, result := range results { if result.Err != nil { return nil, "", result.Err } resultEtag += result.Etag out = append(out, result.Value) } data, err := json.Marshal(out) if err != nil { return nil, "", err } return data, resultEtag, nil } mergeJson.Store(cached.Merge(merger, map[string]cached.Value[[]byte]{ "one": one, "two": two, "three": three, "replaceable": lastSuccess, })) // Create a new cache that indents a buffer. This should only be // called if the buffer has changed. indented := cached.Transform[[]byte](func(js []byte, etag string, err error) ([]byte, string, error) { // Get the json from the previous layer of cache, before // we indent. if err != nil { return nil, "", err } var out bytes.Buffer json.Indent(&out, js, "", "\t") return out.Bytes(), etag, nil }, mergeJson) // We have "clients" that constantly pulls the indented format. go func() { for { if _, _, err := indented.Get(); err != nil { panic(fmt.Errorf("invalid error: %v", err)) } } }() failure := cached.Result[[]byte]{Err: errors.New("Invalid cache!")} // Insert a new sub-cache that fails, it should just be ignored. mergeJson.Store(cached.Merge(merger, map[string]cached.Value[[]byte]{ "one": one, "two": two, "three": three, "replaceable": lastSuccess, "failure": failure, })) // We can replace just a branch of the dependency tree. lastSuccess.Store(cached.Static([]byte("five"), "five")) // We can replace to remove the failure and one of the sub-cached. mergeJson.Store(cached.Merge(merger, map[string]cached.Value[[]byte]{ "one": one, "two": two, "replaceable": lastSuccess, })) }
Output:
Index ¶
- type Atomic
- type LastSuccess
- type Replaceable
- type Result
- type Value
- func Func[T any](fn func() (T, string, error)) Value[T]
- func Merge[K comparable, T, V any](mergeFn func(results map[K]Result[T]) (V, string, error), ...) Value[V]
- func MergeList[T, V any](mergeFn func(results []Result[T]) (V, string, error), delegates []Value[T]) Value[V]
- func Once[T any](d Value[T]) Value[T]
- func Static[T any](value T, etag string) Value[T]
- func Transform[T, V any](transformerFn func(T, string, error) (V, string, error), source Value[T]) Value[V]
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Atomic ¶
type Atomic[T any] struct { // contains filtered or unexported fields }
Atomic wraps a Value[T] as an atomic value that can be replaced. It implements Replaceable[T].
type LastSuccess ¶
LastSuccess calls Value[T].Get(), but hides errors by returning the last success if there has been any.
func (*LastSuccess[T]) Get ¶
func (c *LastSuccess[T]) Get() (T, string, error)
type Replaceable ¶
Replaceable extends the Value[T] interface with the ability to change the underlying Value[T] after construction.
type Result ¶
Result is wrapping T and error into a struct for cases where a tuple is more convenient or necessary in Golang.
type Value ¶
Value is wrapping a value behind a getter for lazy evaluation.
func Merge ¶
func Merge[K comparable, T, V any](mergeFn func(results map[K]Result[T]) (V, string, error), caches map[K]Value[T]) Value[V]
Merge merges a of cached values. The merge function only gets called if any of the dependency has changed.
If any of the dependency returned an error before, or any of the dependency returned an error this time, or if the mergeFn failed before, then the function is run again.
Note that this assumes there is no "partial" merge, the merge function will remerge all the dependencies together everytime. Since the list of dependencies is constant, there is no way to save some partial merge information either.
Also note that Golang map iteration is not stable. If the mergeFn depends on the order iteration to be stable, it will need to implement its own sorting or iteration order.
func MergeList ¶
func MergeList[T, V any](mergeFn func(results []Result[T]) (V, string, error), delegates []Value[T]) Value[V]
MergeList merges a list of cached values. The function only gets called if any of the dependency has changed.
The benefit of ListMerger over the basic Merger is that caches are stored in an ordered list so the order of the cache will be preserved in the order of the results passed to the mergeFn.
If any of the dependency returned an error before, or any of the dependency returned an error this time, or if the mergeFn failed before, then the function is reran.
Note that this assumes there is no "partial" merge, the merge function will remerge all the dependencies together everytime. Since the list of dependencies is constant, there is no way to save some partial merge information either.
func Transform ¶
func Transform[T, V any](transformerFn func(T, string, error) (V, string, error), source Value[T]) Value[V]
Transform the result of another cached value. The transformFn will only be called if the source has updated, otherwise, the result will be returned.
If the dependency returned an error before, or it returns an error this time, or if the transformerFn failed before, the function is reran.