{"id":36486038,"url":"https://github.com/seaguest/cache","last_synced_at":"2026-01-12T01:51:22.912Z","repository":{"id":44746062,"uuid":"203784824","full_name":"seaguest/cache","owner":"seaguest","description":"A lightweight high-performance distributed two-level cache (in-memory + redis) with loader function library for Go.","archived":false,"fork":false,"pushed_at":"2025-08-15T04:10:03.000Z","size":567,"stargazers_count":154,"open_issues_count":1,"forks_count":22,"subscribers_count":6,"default_branch":"master","last_synced_at":"2025-08-15T06:13:02.755Z","etag":null,"topics":["cache","cache-aside","distributed","redis"],"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/seaguest.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,"zenodo":null}},"created_at":"2019-08-22T11:52:47.000Z","updated_at":"2025-08-15T04:10:06.000Z","dependencies_parsed_at":"2024-06-20T05:38:41.066Z","dependency_job_id":"22e21c72-002a-4121-9cd8-8a29fb96fdcb","html_url":"https://github.com/seaguest/cache","commit_stats":null,"previous_names":[],"tags_count":26,"template":false,"template_full_name":null,"purl":"pkg:github/seaguest/cache","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/seaguest%2Fcache","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/seaguest%2Fcache/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/seaguest%2Fcache/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/seaguest%2Fcache/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/seaguest","download_url":"https://codeload.github.com/seaguest/cache/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/seaguest%2Fcache/sbom","scorecard":{"id":82567,"data":{"date":"2025-08-04","repo":{"name":"github.com/seaguest/cache","commit":"97cf4ed698814c0b2426473561ef52daaf977803"},"scorecard":{"version":"v5.2.1-28-gc1d103a9","commit":"c1d103a9bb9f635ec7260bf9aa0699466fa4be0e"},"score":2.9,"checks":[{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/c1d103a9bb9f635ec7260bf9aa0699466fa4be0e/docs/checks.md#packaging"}},{"name":"Code-Review","score":0,"reason":"Found 0/30 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/c1d103a9bb9f635ec7260bf9aa0699466fa4be0e/docs/checks.md#code-review"}},{"name":"Dangerous-Workflow","score":-1,"reason":"no workflows found","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/c1d103a9bb9f635ec7260bf9aa0699466fa4be0e/docs/checks.md#dangerous-workflow"}},{"name":"SAST","score":0,"reason":"no SAST tool detected","details":["Warn: no pull requests merged into dev branch"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/c1d103a9bb9f635ec7260bf9aa0699466fa4be0e/docs/checks.md#sast"}},{"name":"Token-Permissions","score":-1,"reason":"No tokens found","details":null,"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/c1d103a9bb9f635ec7260bf9aa0699466fa4be0e/docs/checks.md#token-permissions"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/c1d103a9bb9f635ec7260bf9aa0699466fa4be0e/docs/checks.md#binary-artifacts"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/c1d103a9bb9f635ec7260bf9aa0699466fa4be0e/docs/checks.md#maintained"}},{"name":"Pinned-Dependencies","score":-1,"reason":"no dependencies found","details":null,"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/c1d103a9bb9f635ec7260bf9aa0699466fa4be0e/docs/checks.md#pinned-dependencies"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/c1d103a9bb9f635ec7260bf9aa0699466fa4be0e/docs/checks.md#cii-best-practices"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/c1d103a9bb9f635ec7260bf9aa0699466fa4be0e/docs/checks.md#security-policy"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/c1d103a9bb9f635ec7260bf9aa0699466fa4be0e/docs/checks.md#fuzzing"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: MIT License: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/c1d103a9bb9f635ec7260bf9aa0699466fa4be0e/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/c1d103a9bb9f635ec7260bf9aa0699466fa4be0e/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'master'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/c1d103a9bb9f635ec7260bf9aa0699466fa4be0e/docs/checks.md#branch-protection"}},{"name":"Vulnerabilities","score":9,"reason":"1 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: GO-2024-2611 / GHSA-8r3f-844c-mc37"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/c1d103a9bb9f635ec7260bf9aa0699466fa4be0e/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-15T06:13:08.051Z","repository_id":44746062,"created_at":"2025-08-15T06:13:08.051Z","updated_at":"2025-08-15T06:13:08.051Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28331257,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-12T00:36:25.062Z","status":"ssl_error","status_checked_at":"2026-01-12T00:36:15.229Z","response_time":60,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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","cache-aside","distributed","redis"],"created_at":"2026-01-12T01:51:21.721Z","updated_at":"2026-01-12T01:51:22.907Z","avatar_url":"https://github.com/seaguest.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# cache\n\nThis is a high-performance, lightweight distributed caching solution that implements the cache-aside pattern, built upon a combination of in-memory and Redis. The cache architecture includes a singular global Redis instance and multiple in-memory instances. Data changes can be synchronized across all in-memory cache instances depending on the cache update policy.\n\nThe library's design gives priority to data retrieval from the in-memory cache first. If the data isn't found in the local memory cache, it then resorts to the Redis cache. Should the data be unavailable in both caches, the library invokes a loader function to fetch the data, storing it in the cache for future access, thus ensuring an always-on cache.\n\n![alt text](./assets/cache.png \"cache-aside pattern\")\n\n## Features\n\n- **Two-level cache** : in-memory cache first, redis-backed\n- **Easy to use** : simple api with minimum configuration.\n- **Data consistency** : all in-memory instances will be notified by `Pub-Sub`\n  if any value gets deleted, other in-memory instances will update.\n- **Concurrency**: singleflight is used to avoid cache breakdown.\n- **Metrics** : provide callback function to measure the cache metrics.\n\n## Sequence diagram\n\n### cache get policy\n - GetPolicyReturnExpired: return found object even if it has expired.\n - GetPolicyReloadOnExpiry: reload object if found object has expired, then return.\n\n\nThe below sequence diagrams have GetPolicyReturnExpired + UpdatePolicyBroadcast.\n\n### Reload from loader function\n\n```mermaid\nsequenceDiagram\n    participant APP as Application\n    participant M as cache\n    participant L as Local Cache\n    participant L2 as Local Cache2\n    participant S as Shared Cache\n    participant R as LoadFunc(DB)\n\n    APP -\u003e\u003e M: Cache.GetObject()\n    alt reload\n        M -\u003e\u003e R: LoadFunc\n        R --\u003e\u003e M: return from LoadFunc\n        M --\u003e\u003e APP: return\n        M -\u003e\u003e S: redis.Set()\n        M -\u003e\u003e L: notifyAll()\n        M -\u003e\u003e L2: notifyAll()\n    end\n```\n\n### Cache GetObject\n\n```mermaid\nsequenceDiagram\n    participant APP as Application\n    participant M as cache\n    participant L as Local Cache\n    participant L2 as Local Cache2\n    participant S as Shared Cache\n    participant R as LoadFunc(DB)\n\n    APP -\u003e\u003e M: Cache.GetObject()\n    alt Local Cache hit\n        M -\u003e\u003e L: mem.Get()\n        L --\u003e\u003e M: {interface{}, error}\n        M --\u003e\u003e APP: return\n        M --\u003e\u003e R: async reload if expired\n    else Local Cache miss but Shared Cache hit\n        M -\u003e\u003e L: mem.Get()\n        L --\u003e\u003e M: cache miss\n        M -\u003e\u003e S: redis.Get()\n        S --\u003e\u003e M: {interface{}, error}\n        M --\u003e\u003e APP: return\n        M --\u003e\u003e R: async reload if expired\n    else All miss\n        M -\u003e\u003e L: mem.Get()\n        L --\u003e\u003e M: cache miss\n        M -\u003e\u003e S: redis.Get()\n        S --\u003e\u003e M: cache miss\n        M -\u003e\u003e R: sync reload\n        R --\u003e\u003e M: return from reload\n        M --\u003e\u003e APP: return\n    end\n```\n\n\n\n### Delete\n\n```mermaid\nsequenceDiagram\n    participant APP as Application\n    participant M as cache\n    participant L as Local Cache\n    participant L2 as Local Cache2\n    participant S as Shared Cache\n\n    APP -\u003e\u003e M: Cache.Delete()\n    alt Delete\n        M -\u003e\u003e S: redis.Delete()\n        M -\u003e\u003e L: notifyAll()\n        M -\u003e\u003e L2: notifyAll()\n        M --\u003e\u003e APP: return\n    end\n```\n\n### Installation\n\n`go get -u github.com/seaguest/cache`\n\n### API\n\n#### Production Interface\n```go\ntype Cache interface {\n    // GetObject loader function f() will be called in case cache all miss\n    // suggest to use object_type#id as key or any other pattern which can easily extract object, aggregate metric for same object in onMetric\n    GetObject(ctx context.Context, key string, obj any, ttl time.Duration, f func() (any, error), opts ...Option) error\n    \n    Delete(ctx context.Context, key string) error\n}\n```\n\nThe `New()` function returns a `Cache` interface:\n\n```go\nfunc New(options ...Option) Cache\n```\n\n#### Testing Support\n\nFor testing purposes, additional functionality is available through `NewForTesting()`:\n\n```go\nfunc NewForTesting(options ...Option) (*Testing, Cache)\n```\n\nThe `Testing` struct provides methods to manipulate cache state for testing:\n\n```go\ntype Testing struct {\n    // DeleteFromMem allows to delete key from mem, for test purpose\n    func (t *Testing) DeleteFromMem(key string)\n    // DeleteFromRedis allows to delete key from redis, for test purpose\n    func (t *Testing) DeleteFromRedis(key string) error\n}\n```\n\n### Tips\n\n`github.com/seaguest/deepcopy`is adopted for deepcopy, returned value is deepcopied to avoid dirty data.\nplease implement DeepCopy interface if you encounter deepcopy performance trouble.\n\n```go\nfunc (p *TestStruct) DeepCopy() interface{} {\n\tc := *p\n\treturn \u0026c\n}\n```\n\n### Usage\n\n#### Production Usage\n```go\npackage main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"time\"\n\n\t\"github.com/gomodule/redigo/redis\"\n\t\"github.com/seaguest/cache\"\n)\n\ntype TestStruct struct {\n\tName string\n}\n\n// this will be called by deepcopy to improve reflect copy performance\nfunc (p *TestStruct) DeepCopy() interface{} {\n\tc := *p\n\treturn \u0026c\n}\n\nfunc main() {\n\tpool := \u0026redis.Pool{\n\t\tMaxIdle:     1000,\n\t\tMaxActive:   1000,\n\t\tWait:        true,\n\t\tIdleTimeout: 240 * time.Second,\n\t\tTestOnBorrow: func(c redis.Conn, t time.Time) error {\n\t\t\t_, err := c.Do(\"PING\")\n\t\t\treturn err\n\t\t},\n\t\tDial: func() (redis.Conn, error) {\n\t\t\treturn redis.Dial(\"tcp\", \"127.0.0.1:6379\")\n\t\t},\n\t}\n\n\t// Production code uses Cache interface\n\tvar c cache.Cache = cache.New(\n\t\tcache.GetConn(pool.Get),\n\t\tcache.GetPolicy(cache.GetPolicyReturnExpired),\n\t\tcache.Separator(\"#\"),\n\t\tcache.OnMetric(func(key, objectType string, metricType string, count int, elapsedTime time.Duration) {\n\t\t\t// handle metric\n\t\t}),\n\t\tcache.OnError(func(ctx context.Context, err error) {\n\t\t\t// handle error\n\t\t}),\n\t)\n\n\tctx, cancel := context.WithTimeout(context.Background(), time.Second*2)\n\tdefer cancel()\n\n\tvar v TestStruct\n\terr := c.GetObject(ctx, fmt.Sprintf(\"TestStruct#%d\", 100), \u0026v, time.Second*3, func() (any, error) {\n\t\t// data fetch logic to be done here\n\t\ttime.Sleep(time.Millisecond * 1200)\n\t\treturn \u0026TestStruct{Name: \"test\"}, nil\n\t})\n\tlog.Println(v, err)\n}\n```\n\n#### Testing Usage\n```go\npackage mypackage_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gomodule/redigo/redis\"\n\t\"github.com/seaguest/cache\"\n)\n\nfunc TestCacheOperations(t *testing.T) {\n\tpool := \u0026redis.Pool{\n\t\tMaxIdle:     10,\n\t\tMaxActive:   50,\n\t\tWait:        true,\n\t\tIdleTimeout: 240 * time.Second,\n\t\tDial: func() (redis.Conn, error) {\n\t\t\treturn redis.Dial(\"tcp\", \"127.0.0.1:6379\")\n\t\t},\n\t}\n\n\t// Use NewForTesting to get both testing helper and cache\n\ttester, c := cache.NewForTesting(\n\t\tcache.GetConn(pool.Get),\n\t\tcache.Separator(\"#\"),\n\t\tcache.OnError(func(ctx context.Context, err error) {\n\t\t\tt.Logf(\"Cache error: %+v\", err)\n\t\t}),\n\t)\n\t\n\t// Clean up cache for testing\n\ttester.DeleteFromRedis(\"test-key\")\n\ttester.DeleteFromMem(\"test-key\")\n\t\n\t// Test cache functionality\n\tvar result string\n\terr := c.GetObject(context.Background(), \"test-key\", \u0026result, time.Minute, func() (any, error) {\n\t\treturn \"test-value\", nil\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\t\n\tif result != \"test-value\" {\n\t\tt.Errorf(\"Expected 'test-value', got '%s'\", result)\n\t}\n}\n```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fseaguest%2Fcache","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fseaguest%2Fcache","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fseaguest%2Fcache/lists"}