Skip to content

Commit

Permalink
init projects
Browse files Browse the repository at this point in the history
  • Loading branch information
viney-shih committed May 24, 2022
1 parent b4738e2 commit 70c6236
Show file tree
Hide file tree
Showing 23 changed files with 3,715 additions and 1 deletion.
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
# Mac only
.DS_Store

# for IDE
.idea
.vscode


# Binaries for programs and plugins
*.exe
*.exe~
Expand Down
126 changes: 126 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
repos:
# ==========================================================================
# Golang Pre-Commit Hooks | https://github.com/tekwizely/pre-commit-golang
#
# Visit the project home page to learn more about the available Hooks,
# including useful arguments you might want to pass into them.
#
# File-Based Hooks:
# Run against matching staged files individually.
#
# Module-Based Hooks:
# Run against module root folders containing matching staged files.
#
# Package-Based Hooks:
# Run against folders containing one or more staged files.
#
# Repo-Based Hooks:
# Run against the entire repo.
# The hooks only run once (if any matching files are staged),
# and are NOT provided the list of staged files,
#
# My-Cmd-* Hooks
# Allow you to invoke custom tools in varous contexts.
# Can be useful if your favorite tool(s) are not built-in (yet)
#
# Hook Suffixes
# Hooks have suffixes in their name that indicate their targets:
#
# +-----------+--------------+
# | Suffix | Target |
# |-----------+--------------+
# | <none> | Files |
# | -mod | Module |
# | -pkg | Package |
# | -repo | Repo Root |
# | -repo-mod | All Modules |
# | -repo-pkg | All Packages |
# +-----------+--------------+
#
# ! Multiple Hook Invocations
# ! Due to OS command-line-length limits, Pre-Commit can invoke a hook
# ! multiple times if a large number of files are staged.
# ! For file and repo-based hooks, this isn't an issue, but for module
# ! and package-based hooks, there is a potential for the hook to run
# ! against the same module or package multiple times, duplicating any
# ! errors or warnings.
#
# Useful Hook Parameters:
# - id: hook-id
# args: [arg1, arg2, ..., '--'] # Pass options ('--' is optional)
# always_run: true # Run even if no matching files staged
# alias: hook-alias # Create an alias
#
# Passing Options To Hooks:
# If your options contain a reference to an existing file, then you will
# need to use a trailing '--' argument to separate the hook options from
# the modified-file list that Pre-Commit passes into the hook.
# NOTE: For repo-based hooks, '--' is not needed.
#
# Always Run:
# By default, hooks ONLY run when matching file types are staged.
# When configured to "always_run", a hook is executed as if EVERY matching
# file were staged.
#
# Aliases:
# Consider adding aliases to longer-named hooks for easier CLI usage.
# ==========================================================================
- repo: https://github.com/tekwizely/pre-commit-golang
rev: master
hooks:
#
# Go Build
#
- id: go-build-mod
# - id: go-build-pkg
- id: go-build-repo-mod
# - id: go-build-repo-pkg
#
# Go Mod Tidy
#
- id: go-mod-tidy
- id: go-mod-tidy-repo
#
# Go Test
#
- id: go-test-mod
# - id: go-test-pkg
- id: go-test-repo-mod
# - id: go-test-repo-pkg
#
# Go Vet
#
# - id: go-vet
- id: go-vet-mod
# - id: go-vet-pkg
- id: go-vet-repo-mod
# - id: go-vet-repo-pkg
#
# Revive
#
- id: go-revive
- id: go-revive-mod
- id: go-revive-repo-mod
#
# StaticCheck
#
- id: go-staticcheck-mod
# - id: go-staticcheck-pkg
- id: go-staticcheck-repo-mod
# - id: go-staticcheck-repo-pkg
#
# Formatters
#
- id: go-fmt
args: [-w]
- id: go-fmt-repo
args: [-w]
- id: go-imports # replaces go-fmt
args: [-w]
- id: go-imports-repo # replaces go-fmt-repo
args: [-w]
#
# Style Checkers
#
- id: go-lint
#
174 changes: 173 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,174 @@
# go-cache
A library of mixed version of key:value store between in-memory cache and shared cache (i.e. Redis) for Go
A library of mixed version of **key:value** store interacts with private (in-memory) cache and shared cache (i.e. Redis) in Go. It provides `Cache-Aside` strategy when dealing with both, and maintains the consistency of private cache between distributed systems by `Pub-Sub` pattern.

Caching is a common technique that aims to improve the performance and scalability of a system. It does this by temporarily copying frequently accessed data to fast storage close to the application. Distributed applications typically implement either or both of the following strategies when caching data:
- Using a `private cache`, where data is held locally on the computer that's running an instance of an application or service.
- Using a `shared cache`, serving as a common source that can be accessed by multiple processes and machines.

![Using a local private cache with a shared cache](./doc/img/caching.png)
Ref: [https://docs.microsoft.com/en-us/azure/architecture/best-practices/images/caching/caching3.png](https://docs.microsoft.com/en-us/azure/architecture/best-practices/images/caching/caching3.png "Using a local private cache with a shared cache")

Considering the flexibility, efficiency and consistency, we starts to build up our own framework.

## Features
- **Easy to use** : provide a friendly interface to deal with both caching mechnaism by simple configuration. Limit the resource on single instance (pod) as well.
- **Data compression** : provide a customized marshal and unmarshal funciton.
- **Fix concurrency issue** : prevent data racing happened on single instance (pod).
- **Metric** : provide callback functions to measure the performance. (i.e. hit rate, private cache usage, ...)

## Data flow
### Load the cache with `Cache-Aside` strategy
![load](./doc/img/get.png)

### Evict the cache
![evict](./doc/img/del.png)

## Installation
```sh
go get github.com/viney-shih/go-cache
```

## Get Started
### Basic usage: Set-And-Get

By adopting `singleton` pattern, initialize the Factory in main.go at the beginning, and deliver it to each package or business logic.

```go
// Initialize the Factory in main.go
tinyLfu := cache.NewTinyLFU(10000)
rds := cache.NewRedis(redis.NewRing(&redis.RingOptions{
Addrs: map[string]string{
"server1": ":6379",
},
}))

cacheFactory := cache.NewFactory(rds, tinyLfu)
```

Treat it as a common **key:value** store like using Redis. But more advanced, it coordinated the usage between multi-level caching mechanism inside.

```go
type Object struct {
Str string
Num int
}

func Example_setAndGetPattern() {
// We create a group of cache named "set-and-get".
// It uses the shared cache only with TTL of ten seconds.
c := cacheFactory.NewCache([]cache.Setting{
{
Prefix: "set-and-get",
CacheAttributes: map[cache.Type]cache.Attribute{
cache.SharedCacheType: {TTL: 10 * time.Second},
},
},
})

ctx := context.TODO()

// set the cache
obj := &Object{
Str: "value1",
Num: 1,
}
if err := c.Set(ctx, "set-and-get", "key", obj); err != nil {
panic("not expected")
}

// read the cache
container := &Object{}
if err := c.Get(ctx, "set-and-get", "key", container); err != nil {
panic("not expected")
}
fmt.Println(container) // Output: Object{ Str: "value1", Num: 1}

// read the cache but failed
if err := c.Get(ctx, "set-and-get", "no-such-key", container); err != nil {
fmt.Println(err) // Output: errors.New("cache key is missing")
}

// Output:
// &{value1 1}
// cache key is missing
}

```

### Advanced usage: `Cache-Aside` strategy

`GetByFunc()` is the easier way to deal with the cache by implementing the getter function in the parameter. When the cache is missing, it will read the data with the getter function and refill it in cache automatically.

```go
func ExampleCache_GetByFunc() {
// We create a group of cache named "get-by-func".
// It uses the local cache only with TTL of ten minutes.
c := cacheFactory.NewCache([]cache.Setting{
{
Prefix: "get-by-func",
CacheAttributes: map[cache.Type]cache.Attribute{
cache.LocalCacheType: {TTL: 10 * time.Minute},
},
},
})

ctx := context.TODO()
container2 := &Object{}
if err := c.GetByFunc(ctx, "get-by-func", "key2", container2, func() (interface{}, error) {
// The getter is used to generate data when cache missed, and refill it to the cache automatically..
// You can read from DB or other microservices.
// Assume we read from MySQL according to the key "key2" and get the value of Object{Str: "value2", Num: 2}
return Object{Str: "value2", Num: 2}, nil
}); err != nil {
panic("not expected")
}

fmt.Println(container2) // Object{ Str: "value2", Num: 2}

// Output:
// &{value2 2}
}
```

`MGetter` is another approaching way to do this. Set this function durning registering the Setting.

```go
func ExampleService_Create_mGetter() {
// We create a group of cache named "mgetter".
// It uses both shared and local caches with separated TTL of one hour and ten minutes.
c := cacheFactory.NewCache([]cache.Setting{
{
Prefix: "mgetter",
CacheAttributes: map[cache.Type]cache.Attribute{
cache.SharedCacheType: {TTL: time.Hour},
cache.LocalCacheType: {TTL: 10 * time.Minute},
},
MGetter: func(keys ...string) (interface{}, error) {
// The MGetter is used to generate data when cache missed, and refill it to the cache automatically..
// You can read from DB or other microservices.
// Assume we read from MySQL according to the key "key3" and get the value of Object{Str: "value3", Num: 3}
// HINT: remember to return as a slice, and the item order needs to consist with the keys in the parameters.
return []Object{{Str: "value3", Num: 3}}, nil
},
},
})

ctx := context.TODO()
container3 := &Object{}
if err := c.Get(ctx, "mgetter", "key3", container3); err != nil {
panic("not expected")
}

fmt.Println(container3) // Object{ Str: "value3", Num: 3}

// Output:
// &{value3 3}
}
```

[More examples](./example_advanced_test.go)

## References
- https://docs.microsoft.com/en-us/azure/architecture/best-practices/caching
- https://github.com/vmihailenco/go-cache-benchmark
- https://github.com/go-redis/cache
52 changes: 52 additions & 0 deletions adapter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package cache

import (
"context"
"time"
)

// Adapter is the interface when communicating with shared/local caches.
type Adapter interface {
MGet(context context.Context, keys []string) ([]Value, error)
MSet(context context.Context, keyVals map[string][]byte, ttl time.Duration, options ...MSetOptions) error
Del(context context.Context, keys ...string) error
}

// MSetOptions is an alias for functional argument.
type MSetOptions func(opts *msetOptions)

type msetOptions struct {
onCostAdd func(key string, cost int)
onCostEvict func(key string, cost int)
}

// WithOnCostAddFunc sets up the callback when adding the cache with key and cost.
func WithOnCostAddFunc(f func(key string, cost int)) MSetOptions {
return func(opts *msetOptions) {
opts.onCostAdd = f
}
}

// WithOnCostEvictFunc sets up the callback when evicting the cache with key and cost.
func WithOnCostEvictFunc(f func(key string, cost int)) MSetOptions {
return func(opts *msetOptions) {
opts.onCostEvict = f
}
}

func loadMSetOptions(options ...MSetOptions) *msetOptions {
opts := &msetOptions{}
for _, option := range options {
option(opts)
}

return opts
}

// Value is returned by MGet()
type Value struct {
// Valid stands for existing in cache or not.
Valid bool
// Bytes stands for the return value in byte format.
Bytes []byte
}
Loading

0 comments on commit 70c6236

Please sign in to comment.