From 71c3201002e68125736ad3510187f92bb3ec5687 Mon Sep 17 00:00:00 2001 From: Victor Conner <victor@conner.dev> Date: Mon, 8 Jul 2024 12:33:30 +0200 Subject: [PATCH] Improve the documentation for the public API --- .golangci.yml | 54 ++++++--------------------------- cache.go | 81 ++++++++++++++++++++++++++++++++++++++++++++----- distribution.go | 8 ++--- fetch.go | 73 ++++++++++++++++++++++++++++++++++++++------ keys.go | 54 ++++++++++++++++++++++++--------- keys_test.go | 4 +-- metrics.go | 10 +++--- options.go | 2 +- passthrough.go | 69 +++++++++++++++++++++++++++++++++++++---- shard.go | 20 +++++++++++- 10 files changed, 282 insertions(+), 93 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 1f3005c..c9313c7 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -36,18 +36,16 @@ linters: - asciicheck # checks that your code does not contain non-ASCII identifiers - bidichk # checks for dangerous unicode character sequences - bodyclose # checks whether HTTP response body is closed successfully - # - cyclop # checks function and package cyclomatic complexity + - cyclop # checks function and package cyclomatic complexity - dupl # tool for code clone detection - durationcheck # checks for two durations multiplied together - errname # checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error - errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13 - - execinquery # checks query string in Query function which reads your Go src files and warning it finds - exhaustive # checks exhaustiveness of enum switch statements - exportloopref # checks for pointers to enclosing loop variables - forbidigo # forbids identifiers - funlen # tool for detection of long functions - gocheckcompilerdirectives # validates go compiler directive comments (//go:) - # - gochecknoglobals # checks that no global variables exist - gochecknoinits # checks that no init functions are present in Go code - gochecksumtype # checks exhaustiveness on Go "sum types" - gocognit # computes and checks the cognitive complexity of functions @@ -98,36 +96,28 @@ linters: - decorder # checks declaration order and count of types, constants, variables and functions - dogsled # checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) - paralleltest # detects missing usage of t.Parallel() method in your Go test + - tagalign # checks that struct tags are well aligned - ## you may want to enable + ## may want to enable + # - wrapcheck # checks that errors returned from external packages are wrapped #- gci # controls golang package import order and makes it always deterministic #- ginkgolinter # [if you use ginkgo/gomega] enforces standards of using ginkgo and gomega #- godox # detects FIXME, TODO and other comment keywords #- goheader # checks is file header matches to pattern #- inamedparam # [great idea, but too strict, need to ignore a lot of cases by default] reports interfaces with unnamed method parameters #- interfacebloat # checks the number of methods inside an interface - #- ireturn # accept interfaces, return concrete types - #- tagalign # checks that struct tags are well aligned - #- varnamelen # [great idea, but too many false positives] checks that the length of a variable's name matches its scope - #- wrapcheck # checks that errors returned from external packages are wrapped - #- zerologlint # detects the wrong usage of zerolog that a user forgets to dispatch zerolog.Event + # - ireturn # accept interfaces, return concrete types + # - varnamelen # [great idea, but too many false positives] checks that the length of a variable's name matches its scope # - nlreturn # checks for a new line before return and branch statements to increase code clarity ## disabled # - lll # reports long lines - # - exhaustruct # [highly recommend to enable] checks if all structure fields are initialized - #- depguard # [replaced by gomodguard] checks if package imports are in a list of acceptable packages - #- dupword # [useless without config] checks for duplicate words in the source code - #- errchkjson # [don't see profit + I'm against of omitting errors like in the first example https://github.com/breml/errchkjson] checks types passed to the json encoding functions. Reports unsupported types and optionally reports occasions, where the check for the returned error can be omitted - #- forcetypeassert # [replaced by errcheck] finds forced type assertions + # - exhaustruct # checks if all structure fields are initialized #- goerr113 # [too strict] checks the errors handling expressions - #- gofmt # [replaced by goimports] checks whether code was gofmt-ed - #- gofumpt # [replaced by goimports, gofumports is not available yet] checks whether code was gofumpt-ed #- gosmopolitan # reports certain i18n/l10n anti-patterns in your Go codebase #- grouper # analyzes expression groups #- importas # enforces consistent import aliases #- maintidx # measures the maintainability index of each function - #- wsl # [too strict and mostly code is not more readable] whitespace linter forces you to use empty lines #- tagliatelle # checks the struct tags #- contextcheck # Check whether the function uses a non-inherited context. @@ -163,33 +153,9 @@ linters-settings: nlreturn: block-size: 8 - exhaustruct: - # Default: [] - exclude: - - ".*_test.go$" - # std libs - - "^net/http.Client$" - - "^net/http.Cookie$" - - "^net/http.Request$" - - "^net/http.Response$" - - "^net/http.Server$" - - "^net/http.Transport$" - - "^net/url.URL$" - - "^os/exec.Cmd$" - - "^reflect.StructField$" - # public libs - - "^github.com/Shopify/sarama.Config$" - - "^github.com/Shopify/sarama.ProducerMessage$" - - "^github.com/mitchellh/mapstructure.DecoderConfig$" - - "^github.com/prometheus/client_golang/.+Opts$" - - "^github.com/spf13/cobra.Command$" - - "^github.com/spf13/cobra.CompletionOptions$" - - "^github.com/stretchr/testify/mock.Mock$" - - "^github.com/testcontainers/testcontainers-go.+Request$" - - "^github.com/testcontainers/testcontainers-go.FromDockerfile$" - - "^golang.org/x/tools/go/analysis.Analyzer$" - - "^google.golang.org/protobuf/.+Options$" - - "^gopkg.in/yaml.v3.Node$" + gosec: + excludes: + - G404 # Ignore insecure random number source. funlen: # Checks the number of lines in a function. diff --git a/cache.go b/cache.go index 65034a7..447e10e 100644 --- a/cache.go +++ b/cache.go @@ -135,6 +135,14 @@ func (c *Client[T]) getWithState(key string) (value T, exists, markedAsMissing, } // Get retrieves a single value from the cache. +// +// Parameters: +// +// key - The key to be retrieved. +// +// Returns: +// +// The value corresponding to the key and a boolean indicating if the value was found. func (c *Client[T]) Get(key string) (T, bool) { shard := c.getShard(key) val, ok, markedAsMissing, refresh := shard.get(key) @@ -143,6 +151,14 @@ func (c *Client[T]) Get(key string) (T, bool) { } // GetMany retrieves multiple values from the cache. +// +// Parameters: +// +// keys - The list of keys to be retrieved. +// +// Returns: +// +// A map of keys to their corresponding values. func (c *Client[T]) GetMany(keys []string) map[string]T { records := make(map[string]T, len(keys)) for _, key := range keys { @@ -155,9 +171,18 @@ func (c *Client[T]) GetMany(keys []string) map[string]T { // GetManyKeyFn follows the same API as GetOrFetchBatch and PassthroughBatch. // You provide it with a slice of IDs and a keyFn, which is applied to create -// the cache key. The returned map uses the IDs as keys instead of the cache key. -// If you've used ScanKeys to retrieve the actual keys, you can retrieve the records -// using GetMany instead. +// the cache key. The returned map uses the IDs as keys instead of the cache +// key. If you've used ScanKeys to retrieve the actual keys, you can retrieve +// the records using GetMany instead. +// +// Parameters: +// +// ids - The list of IDs to be retrieved. +// keyFn - A function that generates the cache key for each ID. +// +// Returns: +// +// A map of IDs to their corresponding values. func (c *Client[T]) GetManyKeyFn(ids []string, keyFn KeyFn) map[string]T { records := make(map[string]T, len(ids)) for _, id := range ids { @@ -168,7 +193,16 @@ func (c *Client[T]) GetManyKeyFn(ids []string, keyFn KeyFn) map[string]T { return records } -// Set writes a single value to the cache. Returns true if it triggered an eviction. +// Set writes a single value to the cache. +// +// Parameters: +// +// key - The key to be set. +// value - The value to be associated with the key. +// +// Returns: +// +// A boolean indicating if the set operation triggered an eviction. func (c *Client[T]) Set(key string, value T) bool { shard := c.getShard(key) return shard.set(key, value, false) @@ -181,7 +215,15 @@ func (c *Client[T]) StoreMissingRecord(key string) bool { return shard.set(key, zero, true) } -// SetMany writes a map of key value pairs to the cache. +// SetMany writes a map of key-value pairs to the cache. +// +// Parameters: +// +// records - A map of keys to values to be set in the cache. +// +// Returns: +// +// A boolean indicating if any of the set operations triggered an eviction. func (c *Client[T]) SetMany(records map[string]T) bool { var triggeredEviction bool for key, value := range records { @@ -193,9 +235,18 @@ func (c *Client[T]) SetMany(records map[string]T) bool { return triggeredEviction } -// SetManyKeyFn follows the same API as GetOrFetchBatch and PassThroughBatch. It -// takes a map of records where the keyFn is applied to each key in the map +// SetManyKeyFn follows the same API as GetOrFetchBatch and PassthroughBatch. +// It takes a map of records where the keyFn is applied to each key in the map // before it's stored in the cache. +// +// Parameters: +// +// records - A map of IDs to values to be set in the cache. +// cacheKeyFn - A function that generates the cache key for each ID. +// +// Returns: +// +// A boolean indicating if any of the set operations triggered an eviction. func (c *Client[T]) SetManyKeyFn(records map[string]T, cacheKeyFn KeyFn) bool { var triggeredEviction bool for id, value := range records { @@ -208,6 +259,10 @@ func (c *Client[T]) SetManyKeyFn(records map[string]T, cacheKeyFn KeyFn) bool { } // ScanKeys returns a list of all keys in the cache. +// +// Returns: +// +// A slice of strings representing all the keys in the cache. func (c *Client[T]) ScanKeys() []string { keys := make([]string, 0, c.Size()) for _, shard := range c.shards { @@ -217,6 +272,10 @@ func (c *Client[T]) ScanKeys() []string { } // Size returns the number of entries in the cache. +// +// Returns: +// +// An integer representing the total number of entries in the cache. func (c *Client[T]) Size() int { var sum int for _, shard := range c.shards { @@ -226,12 +285,20 @@ func (c *Client[T]) Size() int { } // Delete removes a single entry from the cache. +// +// Parameters: +// +// key: The key of the entry to be removed. func (c *Client[T]) Delete(key string) { shard := c.getShard(key) shard.delete(key) } // NumKeysInflight returns the number of keys that are currently being fetched. +// +// Returns: +// +// An integer representing the total number of keys that are currently being fetched. func (c *Client[T]) NumKeysInflight() int { c.inFlightMutex.Lock() defer c.inFlightMutex.Unlock() diff --git a/distribution.go b/distribution.go index 6e1d6f8..ec67fd5 100644 --- a/distribution.go +++ b/distribution.go @@ -159,7 +159,7 @@ func distributedBatchFetch[V, T any](c *Client[T], keyFn KeyFn, fetchFn BatchFet } return func(ctx context.Context, ids []string) (map[string]V, error) { - // We need to be able to lookup the ID of the record based on the key. + // We need to be able to look up the ID of the record based on the key. keyIDMap := make(map[string]string, len(ids)) keys := make([]string, 0, len(ids)) for _, id := range ids { @@ -193,7 +193,7 @@ func distributedBatchFetch[V, T any](c *Client[T], keyFn KeyFn, fetchFn BatchFet // If distributedStaleStorage isn't enabled it means all records are fresh, otherwise checked the CreatedAt time. if !c.distributedEarlyRefreshes || c.clock.Since(record.CreatedAt) < c.distributedRefreshAfterDuration { - // We never wan't to return missing records. + // We never want to return missing records. if !record.IsMissingRecord { fresh[id] = record.Value } else { @@ -205,7 +205,7 @@ func distributedBatchFetch[V, T any](c *Client[T], keyFn KeyFn, fetchFn BatchFet idsToRefresh = append(idsToRefresh, id) c.reportDistributedRefresh() - // We never wan't to return missing records. + // We never want to return missing records. if !record.IsMissingRecord { stale[id] = record.Value } else { @@ -218,7 +218,7 @@ func distributedBatchFetch[V, T any](c *Client[T], keyFn KeyFn, fetchFn BatchFet } dataSourceResponses, err := fetchFn(ctx, idsToRefresh) - // Incase of an error, we'll proceed with the ones we got from the distributed storage. + // In case of an error, we'll proceed with the ones we got from the distributed storage. if err != nil { for i := 0; i < len(stale); i++ { c.reportDistributedStaleFallback() diff --git a/fetch.go b/fetch.go index 07c94b8..09f24fb 100644 --- a/fetch.go +++ b/fetch.go @@ -57,15 +57,40 @@ func getFetch[V, T any](ctx context.Context, c *Client[T], key string, fetchFn F } // GetOrFetch attempts to retrieve the specified key from the cache. If the value -// is absent, it invokes the "fetchFn" function to obtain it and then stores -// the result. Additionally, when background refreshes is enabled, GetOrFetch -// determines if the record needs refreshing and, if necessary, schedules this -// task for background execution. +// is absent, it invokes the fetchFn function to obtain it and then stores the result. +// Additionally, when background refreshes are enabled, GetOrFetch determines if the record +// needs refreshing and, if necessary, schedules this task for background execution. +// +// Parameters: +// +// ctx - The context to be used for the request. +// key - The key to be fetched. +// fetchFn - Used to retrieve the data from the underlying data source if the key is not found in the cache. +// +// Returns: +// +// The value corresponding to the key and an error if one occurred. func (c *Client[T]) GetOrFetch(ctx context.Context, key string, fetchFn FetchFn[T]) (T, error) { return getFetch[T, T](ctx, c, key, fetchFn) } // GetOrFetch is a convenience function that performs type assertion on the result of client.GetOrFetch. +// +// Parameters: +// +// ctx - The context to be used for the request. +// c - The cache client. +// key - The key to be fetched. +// fetchFn - Used to retrieve the data from the underlying data source if the key is not found in the cache. +// +// Returns: +// +// The value corresponding to the key and an error if one occurred. +// +// Type Parameters: +// +// V - The type returned by the fetchFn. Must be assignable to T. +// T - The type stored in the cache. func GetOrFetch[V, T any](ctx context.Context, c *Client[T], key string, fetchFn FetchFn[V]) (V, error) { res, err := getFetch[V, T](ctx, c, key, fetchFn) return unwrap[V](res, err) @@ -104,16 +129,46 @@ func getFetchBatch[V, T any](ctx context.Context, c *Client[T], ids []string, ke return cachedRecords, nil } -// GetOrFetchBatch attempts to retrieve the specified ids from the cache. If any -// of the values are absent, it invokes the fetchFn function to obtain them and -// then stores the result. Additionally, when background refreshes is enabled, -// GetOrFetch determines if any of the records needs refreshing and, if +// GetOrFetchBatch attempts to retrieve the specified ids from the cache. If +// any of the values are absent, it invokes the fetchFn function to obtain them +// and then stores the result. Additionally, when background refreshes are +// enabled, GetOrFetch determines if any of the records need refreshing and, if // necessary, schedules this to be performed in the background. +// +// Parameters: +// +// ctx - The context to be used for the request. +// ids - The list of IDs to be fetched. +// keyFn - Used to generate the cache key for each ID. +// fetchFn - Used to retrieve the data from the underlying data source if any IDs are not found in the cache. +// +// Returns: +// +// A map of IDs to their corresponding values and an error if one occurred. func (c *Client[T]) GetOrFetchBatch(ctx context.Context, ids []string, keyFn KeyFn, fetchFn BatchFetchFn[T]) (map[string]T, error) { return getFetchBatch[T, T](ctx, c, ids, keyFn, fetchFn) } -// GetOrFetchBatch is a convenience function that performs type assertion on the result of client.GetOrFetchBatch. +// GetOrFetchBatch is a convenience function that performs type assertion on the +// result of client.GetOrFetchBatch. +// +// Parameters: +// +// ctx - The context to be used for the request. +// c - The cache client. +// ids - The list of IDs to be fetched. +// keyFn - Used to prefix each ID in order to create a unique cache key. +// fetchFn - Used to retrieve the data from the underlying data source. +// +// Returns: +// +// A map of ids to their corresponding values and an error if one occurred. +// +// Type Parameters: +// +// V - The type returned by the fetchFn. Must be assignable to T. +// T - The type stored in the cache. + func GetOrFetchBatch[V, T any](ctx context.Context, c *Client[T], ids []string, keyFn KeyFn, fetchFn BatchFetchFn[V]) (map[string]V, error) { res, err := getFetchBatch[V, T](ctx, c, ids, keyFn, fetchFn) return unwrapBatch[V](res, err) diff --git a/keys.go b/keys.go index 0820a9a..fd41153 100644 --- a/keys.go +++ b/keys.go @@ -76,13 +76,22 @@ func (c *Client[T]) handleTime(v reflect.Value) string { return "empty-time" } -// Permutated key takes a prefix, and a struct where the fields are -// concatenated with in order to make a unique cache key. Passing -// anything but a struct for "permutationStruct" will result in a panic. +// PermutatedKey takes a prefix and a struct where the fields are concatenated +// in order to create a unique cache key. Passing anything but a struct for +// "permutationStruct" will result in a panic. The cache will only use the +// EXPORTED fields of the struct to construct the key. The permutation struct +// should be FLAT, with no nested structs. The fields can be any of the basic +// types, as well as slices and time.Time values. // -// The cache will only use the EXPORTED fields of the struct to construct the key. -// The permutation struct should be FLAT, with no nested structs. The fields can -// be any of the basic types, as well as slices and time.Time values. +// Parameters: +// +// prefix - The prefix for the cache key. +// permutationStruct - A struct whose fields are concatenated to form a unique cache key. +// Only exported fields are used. +// +// Returns: +// +// A string to be used as the cache key. func (c *Client[T]) PermutatedKey(prefix string, permutationStruct interface{}) string { var sb strings.Builder sb.WriteString(prefix) @@ -148,8 +157,17 @@ func (c *Client[T]) PermutatedKey(prefix string, permutationStruct interface{}) return sb.String() } -// BatchKeyFn provides a function for that can be used in conjunction with "GetOrFetchBatch". -// It takes in a prefix, and returns a function that will append an ID suffix for each item. +// BatchKeyFn provides a function that can be used in conjunction with +// "GetOrFetchBatch". It takes in a prefix and returns a function that will +// append the ID as a suffix for each item. +// +// Parameters: +// +// prefix - The prefix to be used for each cache key. +// +// Returns: +// +// A function that takes an ID and returns a cache key string with the given prefix and ID. func (c *Client[T]) BatchKeyFn(prefix string) KeyFn { return func(id string) string { return fmt.Sprintf("%s-ID-%s", prefix, id) @@ -157,13 +175,21 @@ func (c *Client[T]) BatchKeyFn(prefix string) KeyFn { } // PermutatedBatchKeyFn provides a function that can be used in conjunction -// with GetOrFetchBatch. It takes a prefix, and a struct where the fields are -// concatenated with the id in order to make a unique cache key. Passing -// anything but a struct for "permutationStruct" will result in a panic. +// with GetOrFetchBatch. It takes a prefix and a struct where the fields are +// concatenated with the ID in order to make a unique cache key. Passing +// anything but a struct for "permutationStruct" will result in a panic. The +// cache will only use the EXPORTED fields of the struct to construct the key. +// The permutation struct should be FLAT, with no nested structs. The fields +// can be any of the basic types, as well as slices and time.Time values. +// +// Parameters: +// +// prefix - The prefix for the cache key. +// permutationStruct - A struct whose fields are concatenated to form a unique cache key. Only exported fields are used. +// +// Returns: // -// The cache will only use the EXPORTED fields of the struct to construct the key. -// The permutation struct should be FLAT, with no nested structs. The fields can -// be any of the basic types, as well as slices and time.Time values. +// A function that takes an ID and returns a cache key string with the given prefix, permutation struct fields, and ID. func (c *Client[T]) PermutatedBatchKeyFn(prefix string, permutationStruct interface{}) KeyFn { return func(id string) string { key := c.PermutatedKey(prefix, permutationStruct) diff --git a/keys_test.go b/keys_test.go index 9cb2b04..f86c6fe 100644 --- a/keys_test.go +++ b/keys_test.go @@ -73,7 +73,7 @@ func TestTimeBasedKeys(t *testing.T) { } } -func TestPermutatedRelativeTimeKeys(t *testing.T) { +func TestPermutedTimeKeys(t *testing.T) { t.Parallel() c := sturdyc.New[any](100, 1, time.Hour, 5, @@ -169,7 +169,7 @@ func TestPermutatedKeyHandlesEmptySlices(t *testing.T) { } } -func TestPermutatedBatchKeyFn(t *testing.T) { +func TestPermutedBatchKeyFn(t *testing.T) { t.Parallel() c := sturdyc.New[any](100, 1, time.Hour, 5, diff --git a/metrics.go b/metrics.go index ba9f11f..55ea6c5 100644 --- a/metrics.go +++ b/metrics.go @@ -8,18 +8,18 @@ type MetricsRecorder interface { // Refresh is called when a get operation results in a refresh. Refresh() // MissingRecord is called every time the cache is asked to - // lookup a key which has been marked as missing. + // look up a key which has been marked as missing. MissingRecord() // ForcedEviction is called when the cache reaches its capacity, and has to // evict keys in order to write a new one. ForcedEviction() - // EntiresEvicted is called when the cache evicts keys from a shard. + // EntriesEvicted is called when the cache evicts keys from a shard. EntriesEvicted(int) // ShardIndex is called to report which shard it was that performed an operation. ShardIndex(int) // CacheBatchRefreshSize is called to report the size of the batch refresh. CacheBatchRefreshSize(size int) - // ObserveCacheSize is called to report the size of the cache.metri + // ObserveCacheSize is called to report the size of the cache. ObserveCacheSize(callback func() int) } @@ -27,12 +27,12 @@ type DistributedMetricsRecorder interface { MetricsRecorder // DistributedCacheHit is called for every key that results in a cache hit. DistributedCacheHit() - // DistributedCacheHit is called for every key that results in a cache miss. + // DistributedCacheMiss is called for every key that results in a cache miss. DistributedCacheMiss() // DistributedRefresh is called when we retrieve a record from // the distributed storage that should be refreshed. DistributedRefresh() - // DistributetedMissingRecord is called when we retrieve a record from the + // DistributedMissingRecord is called when we retrieve a record from the // distributed storage that has been marked as a missing record. DistributedMissingRecord() // DistributedFallback is called when you are using a distributed storage diff --git a/options.go b/options.go index eb72750..7322f02 100644 --- a/options.go +++ b/options.go @@ -91,7 +91,7 @@ func WithRelativeTimeKeyFormat(truncation time.Duration) Option { // WithLog allows you to set a custom logger for the cache. The cache isn't chatty, // and will only log warnings and errors that would be a nightmare to debug. If you -// absolutely don't want any logs, you can pass in the sturydc.NoopLogger. +// absolutely don't want any logs, you can pass in the sturdyc.NoopLogger. func WithLog(log Logger) Option { return func(c *Config) { c.log = log diff --git a/passthrough.go b/passthrough.go index ac1a521..829fe53 100644 --- a/passthrough.go +++ b/passthrough.go @@ -4,8 +4,18 @@ import ( "context" ) -// Passthrough is always going to try and retrieve the latest data by calling the -// fetchFn. The cache is used as a fallback if the fetchFn returns an error. +// Passthrough attempts to retrieve the latest data by calling the provided fetchFn. +// If fetchFn encounters an error, the cache is used as a fallback. +// +// Parameters: +// +// ctx - The context to be used for the request. +// key - The key to be fetched. +// fetchFn - Used to retrieve the data from the underlying data source. +// +// Returns: +// +// The value and an error if one occurred and the key was not found in the cache. func (c *Client[T]) Passthrough(ctx context.Context, key string, fetchFn FetchFn[T]) (T, error) { res, err := callAndCache(ctx, c, key, fetchFn) if err == nil { @@ -19,14 +29,43 @@ func (c *Client[T]) Passthrough(ctx context.Context, key string, fetchFn FetchFn return res, err } -// Passthrough is a convenience function that performs type assertion on the result of client.Passthrough. +// Passthrough is a convenience function that performs type assertion on the +// result of client.PassthroughBatch. +// +// Parameters: +// +// ctx - The context to be used for the request. +// c - The cache client. +// key - The key to be fetched. +// fetchFn - Used to retrieve the data from the underlying data source. +// +// Returns: +// +// The value and an error if one occurred and the key was not found in the cache. +// +// Type Parameters: +// +// V - The type returned by the fetchFn. Must be assignable to T. +// T - The type stored in the cache. func Passthrough[T, V any](ctx context.Context, c *Client[T], key string, fetchFn FetchFn[V]) (V, error) { value, err := c.Passthrough(ctx, key, wrap[T](fetchFn)) return unwrap[V](value, err) } -// PassthroughBatch is always going to try and retrieve the latest data by calling -// the fetchFn. The cache is used as a fallback if the fetchFn returns an error. +// PassthroughBatch attempts to retrieve the latest data by calling the provided fetchFn. +// If fetchFn encounters an error, the cache is used as a fallback. +// +// Parameters: +// +// ctx - The context to be used for the request. +// ids - The list of IDs to be fetched. +// keyFn - Used to prefix each ID in order to create a unique cache key. +// fetchFn - Used to retrieve the data from the underlying data source. +// +// Returns: +// +// A map of IDs to their corresponding values, and an error if one occurred and +// none of the IDs were found in the cache. func (c *Client[T]) PassthroughBatch(ctx context.Context, ids []string, keyFn KeyFn, fetchFn BatchFetchFn[T]) (map[string]T, error) { res, err := callAndCacheBatch(ctx, c, callBatchOpts[T, T]{ids, keyFn, fetchFn}) if err == nil { @@ -41,7 +80,25 @@ func (c *Client[T]) PassthroughBatch(ctx context.Context, ids []string, keyFn Ke return res, err } -// Passthrough is a convenience function that performs type assertion on the result of client.PassthroughBatch. +// PassthroughBatch is a convenience function that performs type assertion on the +// result of client.PassthroughBatch. +// +// Parameters: +// +// ctx - The context to be used for the request. +// c - The cache client. +// ids - The list of IDs to be fetched. +// keyFn - Used to prefix each ID in order to create a unique cache key. +// fetchFn - Used to retrieve the data from the underlying data source. +// +// Returns: +// +// A map of ids to their corresponding values and an error if one occurred. +// +// Type Parameters: +// +// V - The type returned by the fetchFn. Must be assignable to T. +// T - The type stored in the cache. func PassthroughBatch[V, T any](ctx context.Context, c *Client[T], ids []string, keyFn KeyFn, fetchFn BatchFetchFn[V]) (map[string]V, error) { res, err := c.PassthroughBatch(ctx, ids, keyFn, wrapBatch[T](fetchFn)) return unwrapBatch[V](res, err) diff --git a/shard.go b/shard.go index 09e05c2..f3813bf 100644 --- a/shard.go +++ b/shard.go @@ -6,6 +6,7 @@ import ( "time" ) +// shard is a thread-safe data structure that holds a subset of the cache entries. type shard[T any] struct { sync.RWMutex *Config @@ -15,6 +16,7 @@ type shard[T any] struct { evictionPercentage int } +// newShard creates a new shard and returns a pointer to it. func newShard[T any](capacity int, ttl time.Duration, evictionPercentage int, cfg *Config) *shard[T] { return &shard[T]{ Config: cfg, @@ -25,6 +27,7 @@ func newShard[T any](capacity int, ttl time.Duration, evictionPercentage int, cf } } +// size returns the number of entries in the shard. func (s *shard[T]) size() int { s.RLock() defer s.RUnlock() @@ -66,6 +69,18 @@ func (s *shard[T]) forceEvict() { s.reportEntriesEvicted(entriesEvicted) } +// get retrieves attempts to retrieve a value from the shard. +// +// Parameters: +// +// key: The key for which the value is to be retrieved. +// +// Returns: +// +// val: The value associated with the key, if it exists. +// exists: A boolean indicating if the value exists in the shard. +// markedAsMissing: A boolean indicating if the key has been marked as a missing record. +// refresh: A boolean indicating if the value should be refreshed in the background. func (s *shard[T]) get(key string) (val T, exists, markedAsMissing, refresh bool) { s.RLock() item, ok := s.entries[key] @@ -106,7 +121,8 @@ func (s *shard[T]) get(key string) (val T, exists, markedAsMissing, refresh bool return item.value, true, item.isMissingRecord, false } -// set sets a key-value pair in the shard. Returns true if it triggered an eviction. +// set writes a key-value pair to the shard and returns a +// boolean indicating whether an eviction was performed. func (s *shard[T]) set(key string, value T, isMissingRecord bool) bool { s.Lock() defer s.Unlock() @@ -147,12 +163,14 @@ func (s *shard[T]) set(key string, value T, isMissingRecord bool) bool { return evict } +// delete removes a key from the shard. func (s *shard[T]) delete(key string) { s.Lock() defer s.Unlock() delete(s.entries, key) } +// keys returns all non-expired keys in the shard. func (s *shard[T]) keys() []string { s.RLock() defer s.RUnlock()