Skip to content

Commit 036fefb

Browse files
committed
Distributed cache support (#38)
* feat: Support distributed cache with olric * feat: Support external distributed cache * feat: Fix lint * feat: Fix docker-compose to be able to build and tests on CI * feat: Fix docker-compose and create directory for olric configuration * feat: Fix docker-compose * feat: Fix * Update the doc * feat: Update plantuml
1 parent 9d9db63 commit 036fefb

32 files changed

+442
-141
lines changed

.traefik.yml

-18
This file was deleted.

Makefile

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ DC_BUILD=$(DC) build
55
DC_EXEC=$(DC) exec
66

77
build-app: env-prod ## Build containers with prod env vars
8-
$(DC_BUILD) souin
8+
$(DC_BUILD) olric souin
99
$(MAKE) up
1010

1111
build-and-run-caddy: ## Build caddy binary
1212
cd plugins/caddy && xcaddy build --with github.com/darkweak/souin/plugins/caddy=./ --with github.com/darkweak/souin@latest=../.. && ./caddy run
1313

1414
build-dev: env-dev ## Build containers with dev env vars
15-
$(DC_BUILD) souin
15+
$(DC_BUILD) olric souin
1616
$(MAKE) up
1717

1818
coverage: ## Show code coverage

README.md

+19-7
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,10 @@ default_cache:
7070
- Authorization
7171
cache_providers:
7272
- all # Enable all providers by default
73-
redis: # Redis configuration
73+
redis: # Redis provider configuration
7474
url: 'redis:6379'
75+
olric: # Olric provider configuration
76+
url: 'olric:3320'
7577
regex:
7678
exclude: 'ARegexHere' # Regex to exclude from cache
7779
ssl_providers: # The {providers}.json to use
@@ -100,6 +102,7 @@ urls:
100102
| `default_cache.headers` | List of headers to include to the cache | `- Authorization`<br/><br/>`- Content-Type`<br/><br/>`- X-Additional-Header` |
101103
| `default_cache.cache_providers` | Your providers list to cache your data, by default it will use all systems | `- all`<br/><br/>`- ristretto`<br/><br/>`- redis` |
102104
| `default_cache.redis.url` | The redis url, used if you enabled it in the provider section | `redis:6379` (container way) and `http://yourdomain.com:6379` (network way) |
105+
| `default_cache.olric.url` | The olric url, used if you enabled it in the provider section | `olric:3320` (container way) and `http://yourdomain.com:3320` (network way) |
103106
| `default_cache.regex.exclude` | The regex used to prevent paths being cached | `^[A-z]+.*$` |
104107
| `ssl_providers` | List of your providers handling certificates | `- traefik`<br/><br/>`- nginx`<br/><br/>`- apache` |
105108
| `urls.{your url or regex}` | List of your custom configuration depending each URL or regex | 'https:\/\/yourdomain.com' |
@@ -136,13 +139,13 @@ See the sequence for the minimal version below
136139

137140
## Cache systems
138141
Supported providers
139-
- [Redis](https://github.com/go-redis/redis)
140-
- [Olric](https://github.com/buraksezer/olric)
142+
- [Redis](https://github.com/go-redis/redis)
143+
- [Olric](https://github.com/buraksezer/olric)
141144

142-
The cache system sits on top of three providers at the moment. It provides an in-memory, redis and Olric cache systems because setting, getting, updating and deleting keys in these providers is as easy as it gets.
143-
In order to do that, Redis and Olric providers need to be either on the same network as the Souin instance when using docker-compose or over the internet, then it will use by default in-memory to avoid network latency as much as possible.
144-
Souin will return at first the in-memory response when it gives a non-empty response, then the olric followed by the redis one with same condition, or fallback to the reverse proxy otherwise.
145-
Since 1.4.2, Souin supports [Olric](https://github.com/buraksezer/olric) to handle distributed cache.
145+
The cache system sits on top of three providers at the moment. It provides an in-memory, redis and Olric cache systems because setting, getting, updating and deleting keys in these providers is as easy as it gets.
146+
In order to do that, Redis and Olric providers need to be either on the same network as the Souin instance when using docker-compose or over the internet, then it will use by default in-memory to avoid network latency as much as possible.
147+
Souin will return at first the in-memory response when it gives a non-empty response, then the olric followed by the redis one with same condition, or fallback to the reverse proxy otherwise.
148+
Since 1.4.2, Souin supports [Olric](https://github.com/buraksezer/olric) to handle distributed cache.
146149

147150
### Cache invalidation
148151
The cache invalidation is build for CRUD requests, if you're doing a GET HTTP request, it will serve the cached response when it exists, otherwise the reverse-proxy response will be served.
@@ -194,6 +197,7 @@ services:
194197
- 80:80
195198
- 443:443
196199
depends_on:
200+
- olric
197201
- redis
198202
environment:
199203
GOPATH: /app
@@ -202,6 +206,14 @@ services:
202206
- /anywhere/configuration.yml:/configuration/configuration.yml
203207
<<: *networks
204208
209+
olric:
210+
build:
211+
context: ./olric
212+
dockerfile: Dockerfile-olric
213+
target: olric
214+
restart: on-failure
215+
<<: *networks
216+
205217
redis:
206218
image: redis:alpine
207219
<<: *networks

api/main_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,5 @@ func TestInitialize(t *testing.T) {
2020
if !endpoints[0].IsEnabled() {
2121
errors.GenerateError(t, fmt.Sprintf("Endpoint should be enabled"))
2222
}
23+
prs["olric"].Reset()
2324
}

api/souin_test.go

+6
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ func mockSouinAPI() *SouinAPI {
2424

2525
func TestSouinAPI_BulkDelete(t *testing.T) {
2626
souinMock := mockSouinAPI()
27+
defer souinMock.providers["olric"].Reset()
2728
for _, provider := range souinMock.providers {
2829
provider.Set("key", []byte("value"), tests.GetMatchedURL("key"), 20 * time.Second)
2930
provider.Set("key2", []byte("value"), tests.GetMatchedURL("key"), 20 * time.Second)
@@ -45,6 +46,7 @@ func TestSouinAPI_BulkDelete(t *testing.T) {
4546

4647
func TestSouinAPI_Delete(t *testing.T) {
4748
souinMock := mockSouinAPI()
49+
defer souinMock.providers["olric"].Reset()
4850
for _, provider := range souinMock.providers {
4951
provider.Set("key", []byte("value"), tests.GetMatchedURL("key"), 20 * time.Second)
5052
}
@@ -65,6 +67,7 @@ func TestSouinAPI_Delete(t *testing.T) {
6567

6668
func TestSouinAPI_GetAll(t *testing.T) {
6769
souinMock := mockSouinAPI()
70+
defer souinMock.providers["olric"].Reset()
6871
for _, v := range souinMock.GetAll() {
6972
if len(v) > 0 {
7073
errors.GenerateError(t, "Souin API shouldn't have a record")
@@ -81,6 +84,7 @@ func TestSouinAPI_GetAll(t *testing.T) {
8184
}
8285
}
8386
souinMock.providers["redis"].Delete("key")
87+
souinMock.providers["olric"].Delete("key")
8488
time.Sleep(10 * time.Second)
8589
for _, v := range souinMock.GetAll() {
8690
if len(v) == 1 {
@@ -91,13 +95,15 @@ func TestSouinAPI_GetAll(t *testing.T) {
9195

9296
func TestSouinAPI_GetBasePath(t *testing.T) {
9397
souinMock := mockSouinAPI()
98+
defer souinMock.providers["olric"].Reset()
9499
if souinMock.GetBasePath() != "/souinbasepath" {
95100
errors.GenerateError(t, "Souin API should be enabled")
96101
}
97102
}
98103

99104
func TestSouinAPI_IsEnabled(t *testing.T) {
100105
souinMock := mockSouinAPI()
106+
defer souinMock.providers["olric"].Reset()
101107
if !souinMock.IsEnabled() {
102108
errors.GenerateError(t, "Souin API should be enabled")
103109
}

cache/coalescing/requestCoalescing_test.go

+4-8
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ import (
1010
"testing"
1111
)
1212

13-
func commonInitializer() (*httptest.ResponseRecorder, *http.Request, *types.RetrieverResponseProperties) {
13+
func TestServeResponse(t *testing.T) {
1414
c := tests.MockConfiguration()
15-
prs := providers.InitializeProvider(c)
1615
regexpUrls := helpers.InitializeRegexp(c)
16+
prs := providers.InitializeProvider(c)
17+
defer prs["olric"].Reset()
18+
rc := Initialize()
1719
retriever := &types.RetrieverResponseProperties{
1820
Configuration: c,
1921
Providers: prs,
@@ -23,12 +25,6 @@ func commonInitializer() (*httptest.ResponseRecorder, *http.Request, *types.Retr
2325
r := httptest.NewRequest("GET", "http://"+tests.DOMAIN+tests.PATH, nil)
2426
w := httptest.NewRecorder()
2527

26-
return w, r, retriever
27-
}
28-
29-
func TestServeResponse(t *testing.T) {
30-
rc := Initialize()
31-
w, r, retriever := commonInitializer()
3228
ServeResponse(
3329
w,
3430
r,

cache/providers/abstractProvider.go

+10
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,27 @@ func InitializeProvider(configuration configurationtypes.AbstractConfigurationIn
2020
if len(configuration.GetDefaultCache().Providers) == 0 || contains(configuration.GetDefaultCache().Providers, "all") {
2121
redis, _ := RedisConnectionFactory(configuration)
2222
providers["redis"] = redis
23+
olric, _ := OlricConnectionFactory(configuration)
24+
providers["olric"] = olric
2325
ristretto, _ := RistrettoConnectionFactory(configuration)
2426
providers["ristretto"] = ristretto
2527
} else {
2628
if contains(configuration.GetDefaultCache().Providers, "redis") {
2729
redis, _ := RedisConnectionFactory(configuration)
2830
providers["redis"] = redis
2931
}
32+
if contains(configuration.GetDefaultCache().Providers, "olric") {
33+
olric, _ := OlricConnectionFactory(configuration)
34+
providers["olric"] = olric
35+
}
3036
if contains(configuration.GetDefaultCache().Providers, "ristretto") {
3137
ristretto, _ := RistrettoConnectionFactory(configuration)
3238
providers["ristretto"] = ristretto
3339
}
3440
}
41+
42+
for _, p := range providers {
43+
_ = p.Init()
44+
}
3545
return providers
3646
}

cache/providers/abstractProvider_test.go

+7-54
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,22 @@
11
package providers
22

33
import (
4-
"github.com/darkweak/souin/configuration"
5-
"github.com/darkweak/souin/configurationtypes"
64
"github.com/darkweak/souin/errors"
75
"github.com/darkweak/souin/helpers"
86
"github.com/darkweak/souin/tests"
9-
"log"
10-
"regexp"
117
"testing"
128
)
139

14-
func MockConfiguration() configurationtypes.AbstractConfigurationInterface {
15-
var config configuration.Configuration
16-
e := config.Parse([]byte(`
17-
default_cache:
18-
headers:
19-
- Authorization
20-
port:
21-
web: 80
22-
tls: 443
23-
redis:
24-
url: 'redis:6379'
25-
regex:
26-
exclude: 'ARegexHere'
27-
ttl: 1000
28-
reverse_proxy_url: 'http://traefik'
29-
ssl_providers:
30-
- traefik
31-
urls:
32-
'domain.com/':
33-
ttl: 1000
34-
headers:
35-
- Authorization
36-
'mysubdomain.domain.com':
37-
ttl: 50
38-
headers:
39-
- Authorization
40-
- 'Content-Type'
41-
`))
42-
if e != nil {
43-
log.Fatal(e)
44-
}
45-
return &config
46-
}
47-
48-
func MockInitializeRegexp(configurationInstance configurationtypes.AbstractConfigurationInterface) regexp.Regexp {
49-
u := ""
50-
for k := range configurationInstance.GetUrls() {
51-
if "" != u {
52-
u += "|"
53-
}
54-
u += "(" + k + ")"
55-
}
56-
57-
return *regexp.MustCompile(u)
58-
}
59-
6010
func TestInitializeProvider(t *testing.T) {
6111
c := tests.MockConfiguration()
6212
ps := InitializeProvider(c)
63-
for _, p := range ps {
64-
err := p.Init()
65-
if nil != err {
66-
errors.GenerateError(t, "Init shouldn't crash")
13+
defer ps["olric"].Reset()
14+
for k, p := range ps {
15+
if k != "olric" {
16+
err := p.Init()
17+
if nil != err {
18+
errors.GenerateError(t, "Init shouldn't crash")
19+
}
6720
}
6821
}
6922
}

cache/providers/olricProvider.go

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package providers
2+
3+
import (
4+
"github.com/buraksezer/olric/client"
5+
"github.com/buraksezer/olric/config"
6+
"github.com/darkweak/souin/cache/keysaver"
7+
t "github.com/darkweak/souin/configurationtypes"
8+
"strconv"
9+
"time"
10+
)
11+
12+
// Olric provider type
13+
type Olric struct {
14+
*client.Client
15+
dm *client.DMap
16+
keySaver *keysaver.ClearKey
17+
}
18+
19+
// OlricConnectionFactory function create new Olric instance
20+
func OlricConnectionFactory(configuration t.AbstractConfigurationInterface) (*Olric, error) {
21+
var keySaver *keysaver.ClearKey
22+
if configuration.GetAPI().Souin.Enable {
23+
keySaver = keysaver.NewClearKey()
24+
}
25+
26+
c, err := client.New(&client.Config{
27+
Servers: []string{configuration.GetDefaultCache().Olric.URL},
28+
Client: &config.Client{
29+
DialTimeout: time.Second,
30+
KeepAlive: time.Second,
31+
MaxConn: 10,
32+
},
33+
})
34+
if err != nil {
35+
panic(err)
36+
}
37+
38+
return &Olric{
39+
c,
40+
nil,
41+
keySaver,
42+
}, nil
43+
}
44+
45+
// ListKeys method returns the list of existing keys
46+
func (provider *Olric) ListKeys() []string {
47+
if nil != provider.keySaver {
48+
return provider.keySaver.ListKeys()
49+
}
50+
return []string{}
51+
}
52+
53+
// Get method returns the populated response if exists, empty response then
54+
func (provider *Olric) Get(key string) []byte {
55+
val2, err := provider.dm.Get(key)
56+
57+
if err != nil {
58+
return []byte{}
59+
}
60+
61+
return val2.([]byte)
62+
}
63+
64+
// Set method will store the response in Redis provider
65+
func (provider *Olric) Set(key string, value []byte, url t.URL, duration time.Duration) {
66+
if duration == 0 {
67+
ttl, _ := strconv.Atoi(url.TTL)
68+
duration = time.Duration(ttl)*time.Second
69+
}
70+
71+
err := provider.dm.PutEx(key, value, duration)
72+
if err != nil {
73+
panic(err)
74+
} else {
75+
go func() {
76+
if nil != provider.keySaver {
77+
provider.keySaver.AddKey(key)
78+
}
79+
}()
80+
}
81+
}
82+
83+
// Delete method will delete the response in Redis provider if exists corresponding to key param
84+
func (provider *Olric) Delete(key string) {
85+
go func() {
86+
err := provider.dm.Delete(key)
87+
if err != nil {
88+
panic(err)
89+
} else {
90+
go func() {
91+
if nil != provider.keySaver {
92+
provider.keySaver.DelKey(key, 0)
93+
}
94+
}()
95+
}
96+
}()
97+
}
98+
99+
// Init method will initialize Olric provider if needed
100+
func (provider *Olric) Init() error {
101+
dm := provider.Client.NewDMap("souin-map")
102+
103+
provider.dm = dm
104+
return nil
105+
}
106+
107+
// Reset method will reset or close provider
108+
func (provider *Olric) Reset() {
109+
provider.Client.Close()
110+
}

0 commit comments

Comments
 (0)