Skip to content

Commit 0a153f4

Browse files
committed
Add a package that provides an interface to cache
1 parent 87697d0 commit 0a153f4

File tree

9 files changed

+374
-1
lines changed

9 files changed

+374
-1
lines changed

Makefile

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
2+
.PHONY: test
3+
test:
4+
go test -race -v -coverpkg=./... -covermode=atomic -coverprofile=coverage.txt ./...

NOTICE.txt

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
pbgopy
2+
===============
3+
4+
This product contains a modified part of pipe, distributed by pipe-cd:
5+
6+
* License: https://github.com/pipe-cd/pipe/blob/master/LICENSE (Apache License v2.0)
7+
* Homepage: https://github.com/pipe-cd/pipe
8+

cache/cache.go

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Copyright 2020 The PipeCD Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Package cache provides an interface to temporarily
16+
// store content on a pbgopy server.
17+
package cache
18+
19+
import (
20+
"errors"
21+
)
22+
23+
var (
24+
ErrNotFound = errors.New("not found")
25+
)
26+
27+
// Getter wraps a method to read from cache.
28+
type Getter interface {
29+
Get(key interface{}) (interface{}, error)
30+
}
31+
32+
// Putter wraps a method to write to cache.
33+
type Putter interface {
34+
Put(key interface{}, value interface{}) error
35+
}
36+
37+
// Deleter wraps a method to delete from cache.
38+
type Deleter interface {
39+
Delete(key interface{}) error
40+
}
41+
42+
// Cache groups Getter, Putter and Deleter.
43+
type Cache interface {
44+
Getter
45+
Putter
46+
Deleter
47+
}
48+
49+
type multiGetter struct {
50+
getters []Getter
51+
}
52+
53+
// MultiGetter combines a lit of getters into a single getter.
54+
func MultiGetter(getters ...Getter) Getter {
55+
all := make([]Getter, 0, len(getters))
56+
for _, r := range getters {
57+
if mg, ok := r.(*multiGetter); ok {
58+
all = append(all, mg.getters...)
59+
} else {
60+
all = append(all, r)
61+
}
62+
}
63+
return &multiGetter{
64+
getters: all,
65+
}
66+
}
67+
68+
func (mg *multiGetter) Get(key interface{}) (interface{}, error) {
69+
if len(mg.getters) == 0 {
70+
return nil, ErrNotFound
71+
}
72+
if len(mg.getters) == 1 {
73+
return mg.getters[0].Get(key)
74+
}
75+
var firstErr error
76+
for i := range mg.getters {
77+
e, err := mg.getters[i].Get(key)
78+
if firstErr == nil && err != nil {
79+
firstErr = err
80+
}
81+
if err == nil {
82+
return e, nil
83+
}
84+
}
85+
return nil, firstErr
86+
}

cache/cache_test.go

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright 2020 The PipeCD Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package cache
16+
17+
import (
18+
"errors"
19+
"testing"
20+
21+
"github.com/stretchr/testify/assert"
22+
)
23+
24+
type getterFunc func(key interface{}) (interface{}, error)
25+
26+
func (f getterFunc) Get(key interface{}) (interface{}, error) {
27+
return f(key)
28+
}
29+
30+
func TestMultiGetter(t *testing.T) {
31+
value := "ok"
32+
err := errors.New("err")
33+
var calls int
34+
35+
successGetter := getterFunc(func(key interface{}) (interface{}, error) {
36+
calls++
37+
return value, nil
38+
})
39+
failureGetter := getterFunc(func(key interface{}) (interface{}, error) {
40+
calls++
41+
return nil, err
42+
})
43+
testcases := []struct {
44+
name string
45+
getter Getter
46+
err error
47+
value interface{}
48+
calls int
49+
}{
50+
{
51+
getter: MultiGetter(successGetter),
52+
value: value,
53+
calls: 1,
54+
},
55+
{
56+
getter: MultiGetter(successGetter, successGetter),
57+
value: value,
58+
calls: 1,
59+
},
60+
{
61+
getter: MultiGetter(successGetter, failureGetter),
62+
value: value,
63+
calls: 1,
64+
},
65+
{
66+
getter: MultiGetter(failureGetter, successGetter),
67+
value: value,
68+
calls: 2,
69+
},
70+
{
71+
getter: MultiGetter(failureGetter, MultiGetter(failureGetter, successGetter)),
72+
value: value,
73+
calls: 3,
74+
},
75+
{
76+
getter: MultiGetter(failureGetter, failureGetter),
77+
err: err,
78+
calls: 2,
79+
},
80+
}
81+
for _, tc := range testcases {
82+
t.Run(tc.name, func(t *testing.T) {
83+
calls = 0
84+
value, err := tc.getter.Get("")
85+
assert.Equal(t, tc.err, err)
86+
assert.Equal(t, tc.value, value)
87+
assert.Equal(t, tc.calls, calls)
88+
})
89+
}
90+
}

cache/memorycache/cache.go

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright 2020 The PipeCD Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package memorycache
16+
17+
import (
18+
"sync"
19+
20+
"github.com/nakabonne/pbgopy/cache"
21+
)
22+
23+
// Cache acts a simple cache that is safe for concurrent use
24+
// by multiple goroutines without additional locking or coordination.
25+
type Cache struct {
26+
values sync.Map
27+
}
28+
29+
func NewCache() *Cache {
30+
return &Cache{}
31+
}
32+
33+
func (c *Cache) Get(key interface{}) (interface{}, error) {
34+
item, ok := c.values.Load(key)
35+
if !ok {
36+
return nil, cache.ErrNotFound
37+
}
38+
return item, nil
39+
}
40+
41+
func (c *Cache) Put(key interface{}, value interface{}) error {
42+
c.values.Store(key, value)
43+
return nil
44+
}
45+
46+
func (c *Cache) Delete(key interface{}) error {
47+
c.values.Delete(key)
48+
return nil
49+
}

cache/memorycache/ttl_cache.go

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright 2020 The PipeCD Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package memorycache
16+
17+
import (
18+
"context"
19+
"sync"
20+
"time"
21+
22+
"github.com/nakabonne/pbgopy/cache"
23+
)
24+
25+
type entry struct {
26+
value interface{}
27+
expiration time.Time
28+
}
29+
30+
type TTLCache struct {
31+
entries sync.Map
32+
ttl time.Duration
33+
ctx context.Context
34+
}
35+
36+
func NewTTLCache(ctx context.Context, ttl time.Duration, evictionInterval time.Duration) *TTLCache {
37+
c := &TTLCache{
38+
ttl: ttl,
39+
ctx: ctx,
40+
}
41+
if evictionInterval > 0 {
42+
go c.startEvicter(evictionInterval)
43+
}
44+
return c
45+
}
46+
47+
func (c *TTLCache) startEvicter(interval time.Duration) {
48+
ticker := time.NewTicker(interval)
49+
for {
50+
select {
51+
case now := <-ticker.C:
52+
c.evictExpired(now)
53+
case <-c.ctx.Done():
54+
ticker.Stop()
55+
return
56+
}
57+
}
58+
}
59+
60+
func (c *TTLCache) evictExpired(t time.Time) {
61+
c.entries.Range(func(key interface{}, value interface{}) bool {
62+
e := value.(*entry)
63+
if e.expiration.Before(t) {
64+
c.entries.Delete(key)
65+
}
66+
return true
67+
})
68+
}
69+
70+
func (c *TTLCache) Get(key interface{}) (interface{}, error) {
71+
item, ok := c.entries.Load(key)
72+
if !ok {
73+
return nil, cache.ErrNotFound
74+
}
75+
return item.(*entry).value, nil
76+
}
77+
78+
func (c *TTLCache) Put(key interface{}, value interface{}) error {
79+
e := &entry{
80+
value: value,
81+
expiration: time.Now().Add(c.ttl),
82+
}
83+
c.entries.Store(key, e)
84+
return nil
85+
}
86+
87+
func (c *TTLCache) Delete(key interface{}) error {
88+
c.entries.Delete(key)
89+
return nil
90+
}

cache/memorycache/ttl_cache_test.go

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright 2020 The PipeCD Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package memorycache
16+
17+
import (
18+
"context"
19+
"testing"
20+
"time"
21+
22+
"github.com/stretchr/testify/assert"
23+
"github.com/stretchr/testify/require"
24+
25+
"github.com/nakabonne/pbgopy/cache"
26+
)
27+
28+
func TestTTLCache(t *testing.T) {
29+
c := NewTTLCache(context.TODO(), 0, 5*time.Second)
30+
err := c.Put("key-1", "value-1")
31+
require.NoError(t, err)
32+
value, err := c.Get("key-1")
33+
require.NoError(t, err)
34+
assert.Equal(t, "value-1", value)
35+
36+
c.evictExpired(time.Now())
37+
value, err = c.Get("key-1")
38+
assert.Equal(t, cache.ErrNotFound, err)
39+
assert.Equal(t, nil, value)
40+
}

go.mod

+4-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,7 @@ module github.com/nakabonne/pbgopy
22

33
go 1.15
44

5-
require github.com/spf13/cobra v1.1.1
5+
require (
6+
github.com/spf13/cobra v1.1.1
7+
github.com/stretchr/testify v1.3.0
8+
)

0 commit comments

Comments
 (0)