Skip to content

Commit

Permalink
initial implement
Browse files Browse the repository at this point in the history
  • Loading branch information
Songmu committed Mar 20, 2020
0 parents commit 7bf86d9
Show file tree
Hide file tree
Showing 10 changed files with 305 additions and 0 deletions.
44 changes: 44 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: test
on:
push:
branches:
- "**"
pull_request: {}
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
- macOS-latest
- windows-latest
steps:
- name: setup go
uses: actions/setup-go@v1
with:
go-version: 1.x
- name: checkout
uses: actions/checkout@v1
with:
fetch-depth: 1
- name: lint
run: |
GO111MODULE=off GOBIN=$(pwd)/bin go get golang.org/x/lint/golint
bin/golint -set_exit_status ./...
if: "matrix.os == 'ubuntu-latest' || matrix.os == 'macOS-latest'"
- name: test
run: go test -coverprofile coverage.out -covermode atomic ./...
- name: Convert coverage to lcov
uses: jandelgado/gcov2lcov-action@v1.0.0
with:
infile: coverage.out
outfile: coverage.lcov
if: "matrix.os == 'ubuntu-latest'"
- name: Coveralls
uses: coverallsapp/github-action@master
with:
github-token: ${{ secrets.github_token }}
path-to-lcov: coverage.lcov
if: "matrix.os == 'ubuntu-latest'"
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.*
!.gitignore
!.github
22 changes: 22 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
Copyright (c) 2020 Songmu

MIT License

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
30 changes: 30 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
u := $(if $(update),-u)

export GO111MODULE=on

.PHONY: deps
deps:
go get ${u} -d
go mod tidy

.PHONY: devel-deps
devel-deps:
sh -c '\
tmpdir=$$(mktemp -d); \
cd $$tmpdir; \
go get ${u} \
golang.org/x/lint/golint \
github.com/Songmu/godzil/cmd/godzil; \
rm -rf $$tmpdir'

.PHONY: test
test:
go test

.PHONY: lint
lint: devel-deps
golint -set_exit_status

.PHONY: release
release: devel-deps
godzil release
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
smartcache
=======

[![Test Status](https://github.com/Songmu/smartcache/workflows/test/badge.svg?branch=master)][actions]
[![Coverage Status](https://coveralls.io/repos/Songmu/smartcache/badge.svg?branch=master)][coveralls]
[![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)][license]
[![GoDoc](https://godoc.org/github.com/Songmu/smartcache?status.svg)][godoc]

[actions]: https://github.com/Songmu/smartcache/actions?workflow=test
[coveralls]: https://coveralls.io/r/Songmu/smartcache?branch=master
[license]: https://github.com/Songmu/smartcache/blob/master/LICENSE
[godoc]: https://godoc.org/github.com/Songmu/smartcache

smartcache short description

## Synopsis

```go
// simple usage here
```

## Description

## Installation

```console
% go get github.com/Songmu/smartcache
```

## Author

[Songmu](https://github.com/Songmu)
8 changes: 8 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module github.com/Songmu/smartcache

go 1.13

require (
github.com/Songmu/flextime v0.0.6
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a
)
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
github.com/Songmu/flextime v0.0.6 h1:q9uTNwKY014E0AmCGOFt+0AaI5OXRty8gAalRyXdn9c=
github.com/Songmu/flextime v0.0.6/go.mod h1:ofUSZ/qj7f1BfQQ6rEH4ovewJ0SZmLOjBF1xa8iE87Q=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
69 changes: 69 additions & 0 deletions smartcache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package smartcache

import (
"context"
"sync"
"time"

"github.com/Songmu/flextime"
"golang.org/x/sync/singleflight"
)

// Cache for cache
type Cache struct {
expire time.Duration
softExpire time.Duration
generator func(context.Context) (interface{}, error)

group singleflight.Group

mu sync.RWMutex
value interface{}
nextSoftExpire time.Time
nextExpire time.Time
}

// New returns new Cache
func New(expire, softExpire time.Duration, gen func(context.Context) (interface{}, error)) *Cache {
return &Cache{
expire: expire,
softExpire: softExpire,
generator: gen,
}
}

func (ca *Cache) renew(ctx context.Context) (interface{}, error) {
val, err, _ := ca.group.Do("renew", func() (interface{}, error) {
val, err := ca.generator(ctx)
if err == nil {
now := flextime.Now()
ca.mu.Lock()
ca.value = val
if ca.softExpire > 0 {
ca.nextSoftExpire = now.Add(ca.softExpire)
}
ca.nextExpire = now.Add(ca.expire)
ca.mu.Unlock()
}
return val, err
})
return val, err
}

// Get the cached value
func (ca *Cache) Get(ctx context.Context) (interface{}, error) {
now := flextime.Now()
ca.mu.RLock()
currVal := ca.value
softExpire := ca.nextSoftExpire
expire := ca.nextExpire
ca.mu.RUnlock()

if now.After(expire) {
return ca.renew(ctx)
}
if !softExpire.IsZero() && now.After(softExpire) {
go ca.renew(ctx)
}
return currVal, nil
}
90 changes: 90 additions & 0 deletions smartcache_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package smartcache_test

import (
"context"
"sync"
"testing"
"time"

"github.com/Songmu/flextime"
"github.com/Songmu/smartcache"
)

func TestCache_Get(t *testing.T) {
now := flextime.Now()
defer flextime.Fix(now)()

var counter int
ca := smartcache.New(10*time.Second, time.Second, func(ctx context.Context) (interface{}, error) {
// Actually time.Sleep instead of flextime to emulate long running operations
time.Sleep(50 * time.Millisecond)
counter++
return counter, nil
})

t.Run("create new cache", func(t *testing.T) {
v, err := ca.Get(context.Background())
if err != nil {
t.Errorf("error should be nil, but: %s", err)
}
if v.(int) != 1 {
t.Errorf("value should be 1, but %v", v)
}
})

t.Run("get from cache", func(t *testing.T) {
v, err := ca.Get(context.Background())
if err != nil {
t.Errorf("error should be nil, but: %s", err)
}
if v.(int) != 1 {
t.Errorf("value should be 1, but %v", v)
}
})

t.Run("soft expire and renew internal value", func(t *testing.T) {
flextime.Sleep(2 * time.Second)
// check the concurrent cache updates won't conflict
var (
wg = sync.WaitGroup{}
para = 10
)
wg.Add(para)
for i := 0; i < para; i++ {
go func() {
defer wg.Done()
ca.Get(context.Background())
}()
}
wg.Wait()
v, err := ca.Get(context.Background())
if err != nil {
t.Errorf("error should be nil, but: %s", err)
}
if v.(int) != 1 {
t.Errorf("value should be 1, but %v", v)
}
})

t.Run("wait for internal value update", func(t *testing.T) {
time.Sleep(55 * time.Millisecond) // use real time.Sleep for waiting cache update
v, err := ca.Get(context.Background())
if err != nil {
t.Errorf("error should be nil, but: %s", err)
}
if v.(int) != 2 {
t.Errorf("value should be 2, but %v", v)
}
})

t.Run("hard expire", func(t *testing.T) {
flextime.Sleep(11 * time.Second)
v, err := ca.Get(context.Background())
if err != nil {
t.Errorf("error should be nil, but: %s", err)
}
if v.(int) != 3 {
t.Errorf("value should be 3, but %v", v)
}
})
}
3 changes: 3 additions & 0 deletions version.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package smartcache

const version = "0.0.0"

0 comments on commit 7bf86d9

Please sign in to comment.