cached

package
v0.0.0-...-f0e62f9 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 30, 2024 License: Apache-2.0 Imports: 3 Imported by: 3

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

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].

func (*Atomic[T]) Get

func (x *Atomic[T]) Get() (T, string, error)

func (*Atomic[T]) Store

func (x *Atomic[T]) Store(val Value[T])

type LastSuccess

type LastSuccess[T any] struct {
	Atomic[T]
	// contains filtered or unexported fields
}

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

type Replaceable[T any] interface {
	Value[T]
	Store(Value[T])
}

Replaceable extends the Value[T] interface with the ability to change the underlying Value[T] after construction.

type Result

type Result[T any] struct {
	Value T
	Etag  string
	Err   error
}

Result is wrapping T and error into a struct for cases where a tuple is more convenient or necessary in Golang.

func (Result[T]) Get

func (r Result[T]) Get() (T, string, error)

type Value

type Value[T any] interface {
	Get() (value T, etag string, err error)
}

Value is wrapping a value behind a getter for lazy evaluation.

func Func

func Func[T any](fn func() (T, string, error)) Value[T]

Func wraps a (thread-safe) function as a Value[T].

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 Once

func Once[T any](d Value[T]) Value[T]

Once calls Value[T].Get() lazily and only once, even in case of an error result.

func Static

func Static[T any](value T, etag string) Value[T]

Static returns constant values.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL