{"id":18635391,"url":"https://github.com/naughtygopher/pocache","last_synced_at":"2025-04-04T15:05:08.746Z","repository":{"id":257811088,"uuid":"867722582","full_name":"naughtygopher/pocache","owner":"naughtygopher","description":"Pocache is a minimal cache package which focuses on a preemptive optimistic caching strategy","archived":false,"fork":false,"pushed_at":"2025-01-25T09:43:25.000Z","size":50,"stargazers_count":221,"open_issues_count":0,"forks_count":5,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-28T14:04:30.393Z","etag":null,"topics":["cache","caching","caching-library","caching-strategies","go","go-cache","golang","golang-cache"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/naughtygopher.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-10-04T15:38:31.000Z","updated_at":"2025-03-14T21:11:42.000Z","dependencies_parsed_at":null,"dependency_job_id":"d2135342-6ce6-480d-a035-d0b382617d9a","html_url":"https://github.com/naughtygopher/pocache","commit_stats":null,"previous_names":["naughtygopher/incache"],"tags_count":10,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/naughtygopher%2Fpocache","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/naughtygopher%2Fpocache/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/naughtygopher%2Fpocache/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/naughtygopher%2Fpocache/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/naughtygopher","download_url":"https://codeload.github.com/naughtygopher/pocache/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247198435,"owners_count":20900079,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cache","caching","caching-library","caching-strategies","go","go-cache","golang","golang-cache"],"created_at":"2024-11-07T05:24:48.296Z","updated_at":"2025-04-04T15:05:08.722Z","avatar_url":"https://github.com/naughtygopher.png","language":"Go","funding_links":[],"categories":["Database","数据库","Data Integration Frameworks"],"sub_categories":["Caches","缓存"],"readme":"\u003cp align=\"center\"\u003e\u003cimg src=\"https://github.com/user-attachments/assets/1038467d-6058-4227-8a59-cf29b847fb2b\" alt=\"pocache gopher\" width=\"256px\"/\u003e\u003c/p\u003e\n\n[![](https://github.com/naughtygopher/pocache/actions/workflows/go.yml/badge.svg?branch=main)](https://github.com/naughtygopher/pocache/actions)\n[![Go Reference](https://pkg.go.dev/badge/github.com/naughtygopher/pocache.svg)](https://pkg.go.dev/github.com/naughtygopher/pocache)\n[![Go Report Card](https://goreportcard.com/badge/github.com/naughtygopher/pocache?cache_invalidate=v0.3.0)](https://goreportcard.com/report/github.com/naughtygopher/pocache)\n[![Coverage Status](https://coveralls.io/repos/github/naughtygopher/pocache/badge.svg?branch=main\u0026cache_invalidate=v0.3.0)](https://coveralls.io/github/naughtygopher/pocache?branch=main)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/creativecreature/sturdyc/blob/master/LICENSE)\n[![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go?tab=readme-ov-file#caches)\n\n# Pocache\n\nPocache (`poh-cash (/poʊ kæʃ/)`), **P**reemptive **o**ptimistic cache, is a lightweight in-app caching package. It introduces preemptive cache updates, optimizing performance in concurrent environments by reducing redundant database calls while maintaining fresh data. It uses [Hashicorp's Go LRU package](https://github.com/hashicorp/golang-lru) as the default storage.\n\nYet another _elegant_ solution for the infamous [Thundering herd problem](https://en.wikipedia.org/wiki/Thundering_herd_problem), save your database(s)!\n\n## Key Features\n\n1. **Preemptive Cache Updates:** Automatically updates cache entries _nearing_ expiration.\n2. **Threshold Window:** Configurable time window before cache expiration to trigger updates.\n3. **Serve stale**: Opt-in configuration to serve expired cache and do a background refresh.\n4. **Debounced Updates:** Prevents excessive I/O calls by debouncing concurrent update requests for the same key.\n5. **Custom store**: customizable underlying storage to extend/replace in-app cache or use external cache database.\n\n## Why use Pocache?\n\nIn highly concurrent environments (e.g., web servers), multiple requests try to access the same cache entry simultaneously. Without query call suppression / call debouncing, the app would query the underlying database multiple times until the cache is refreshed. While trying to solve the thundering herd problem, most applications serve stale/expired cache until the update is completed.\n\nPocache solves these scenarios by combining debounce mechanism along with optimistic updates during the threshold window, keeping the cache up to date all the time and never having to serve stale cache!\n\n## How does it work?\n\nGiven a cache expiration time and a threshold window, Pocache triggers a preemptive cache update when a value is accessed within the threshold window.\n\nExample:\n\n-   Cache expiration: 10 minutes\n-   Threshold window: 1 minute\n\n```\n|______________________ ____threshold window__________ ______________|\n0 min                   9 mins                         10 mins\nAdd key here            Get key within window          Key expires\n```\n\nWhen a key is fetched within the threshold window (between 9-10 minutes), Pocache initiates a background update for that key (_preemptive_). This ensures fresh data availability, anticipating future usage (_optimistic_).\n\n## Custom store\n\nPocache defines the following interface for its underlying storage. You can configure storage of your choice as long as it implements this simple interface, and is provided as a configuration.\n\n```golang\ntype store[K comparable, T any] interface {\n\tAdd(key K, value *Payload[T]) (evicted bool)\n\tGet(key K) (value *Payload[T], found bool)\n\tRemove(key K) (present bool)\n}\n```\n\nBelow is an example(not for production use) of setting a custom store.\n\n```golang\ntype mystore[Key comparable, T any] struct{\n    data sync.Map\n}\n\nfunc (ms *mystore[K,T]) Add(key K, value *Payload[T]) (evicted bool) {\n    ms.data.Store(key, value)\n}\n\nfunc (ms *mystore[K,T]) Get(key K) (value *Payload[T], found bool) {\n    v, found  := ms.data.Load(key)\n    if !found {\n        return nil, found\n    }\n\n    value, _ := v.(*Payload[T])\n    return value, true\n}\n\nfunc (ms *mystore[K,T]) Remove(key K) (present bool) {\n    _, found  := ms.data.Load(key)\n    ms.data.Delete(key)\n    return found\n}\n\nfunc foo() {\n    cache, err := pocache.New(pocache.Config[string, string]{\n        Store: mystore{data: sync.Map{}}\n\t})\n}\n```\n\n## Full example\n\n```golang\npackage main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/naughtygopher/pocache\"\n)\n\ntype Item struct {\n\tID          string\n\tName        string\n\tDescription string\n}\n\nfunc newItem(key string) *Item {\n\treturn \u0026Item{\n\t\tID:          fmt.Sprintf(\"%d\", time.Now().Nanosecond()),\n\t\tName:        \"name::\" + key,\n\t\tDescription: \"description::\" + key,\n\t}\n}\n\nfunc updater(ctx context.Context, key string) (*Item, error) {\n\treturn newItem(key), nil\n}\n\nfunc onErr(err error) {\n\tpanic(fmt.Sprintf(\"this should never have happened!: %+v\", err))\n}\n\nfunc main() {\n\tcache, err := pocache.New(pocache.Config[string, *Item]{\n\t\t// LRUCacheSize is the number of keys to be maintained in the cache (Optional, default 1000)\n\t\tLRUCacheSize: 100000,\n\t\t// QLength is the length of update and delete queue (Optional, default 1000)\n\t\tQLength: 1000,\n\n\t\t// CacheAge is for how long the cache would be maintained, apart from the LRU eviction\n\t\t// It's maintained to not maintain stale data if/when keys are not evicted based on LRU\n\t\t// (Optional, default 1minute)\n\t\tCacheAge: time.Hour,\n\t\t// Threshold is the duration prior to expiry, when the key is considered eligible to be updated\n\t\t// (Optional, default 1 second)\n\t\tThreshold: time.Minute * 5,\n\n\t\t// ServeStale will not return error if the cache has expired. It will return the stale\n\t\t// value, and trigger an update as well. This is useful for usecases where it's ok\n\t\t// to serve stale values and data consistency is not of paramount importance.\n\t\t// (Optional, default false)\n\t\tServeStale: false,\n\n\t\t// UpdaterTimeout is the context time out for when the updater function is called\n\t\t// (Optional, default 1 second)\n\t\tUpdaterTimeout: time.Second * 15,\n\t\t// Updater is optional, but without it it's a basic LRU cache\n\t\tUpdater: updater,\n\n\t\t// ErrWatcher is called when there's any error when trying to update cache (Optional)\n\t\tErrWatcher: onErr,\n\t})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tconst key = \"hello\"\n\titem := newItem(key)\n\te := cache.Add(key, item)\n\tfmt.Println(\"evicted:\", e)\n\n\tee := cache.BulkAdd([]pocache.Tuple[string, *Item]{\n\t\t{Key: key + \"2\", Value: newItem(key + \"2\")},\n\t})\n\tfmt.Println(\"evicted list:\", ee)\n\n\tii := cache.Get(key)\n\tif ii.Found {\n\t\tfmt.Println(\"value:\", ii.V)\n\t}\n\n\tii = cache.Get(key + \"2\")\n\tif ii.Found {\n\t\tfmt.Println(\"value:\", ii.V)\n\t}\n}\n```\n\n## The gopher\n\nThe gopher used here was created using [Gopherize.me](https://gopherize.me/). Pocache helps you to stop the herd from thundering.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnaughtygopher%2Fpocache","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnaughtygopher%2Fpocache","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnaughtygopher%2Fpocache/lists"}