Skip to content

Commit 5b73208

Browse files
committed
all: support lru and internal backup
1 parent 0e0a592 commit 5b73208

File tree

11 files changed

+292
-24
lines changed

11 files changed

+292
-24
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,6 @@
1515

1616
# directories (remove the comment below to include it)
1717
vendor/
18-
build/
18+
build/
19+
data/redis
20+
data/backup

cache.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package main
2+
3+
import (
4+
"container/list"
5+
"sync"
6+
"time"
7+
)
8+
9+
type item struct {
10+
k, v string
11+
}
12+
13+
// lru is a naive thread-safe lru cache
14+
type lru struct {
15+
cap uint
16+
size uint
17+
elems *list.List // of item
18+
19+
mu sync.RWMutex
20+
}
21+
22+
func newLRU(doexpire bool) *lru {
23+
l := &lru{
24+
cap: 32, // could do it with memory quota
25+
size: 0,
26+
elems: list.New(),
27+
mu: sync.RWMutex{},
28+
}
29+
if doexpire {
30+
go l.clear()
31+
}
32+
return l
33+
}
34+
35+
// clear clears the lru after a while, this is just a dirty
36+
// solution to prevent if the database is updated but lru is
37+
// not synced.
38+
func (l *lru) clear() {
39+
t := time.NewTicker(time.Minute * 5)
40+
for {
41+
select {
42+
case <-t.C:
43+
l.mu.Lock()
44+
for e := l.elems.Front(); e != nil; e = e.Next() {
45+
l.elems.Remove(e)
46+
}
47+
l.mu.Unlock()
48+
}
49+
}
50+
}
51+
52+
func (l *lru) Get(k string) (string, bool) {
53+
l.mu.RLock()
54+
defer l.mu.RUnlock()
55+
56+
for e := l.elems.Front(); e != nil; e = e.Next() {
57+
if e.Value.(*item).k == k {
58+
l.elems.MoveToFront(e)
59+
return e.Value.(*item).v, true
60+
}
61+
}
62+
return "", false
63+
}
64+
65+
func (l *lru) Put(k, v string) {
66+
l.mu.Lock()
67+
defer l.mu.Unlock()
68+
69+
// found from cache
70+
i := &item{k, v}
71+
for e := l.elems.Front(); e != nil; e = e.Next() {
72+
if e.Value.(*item).k == k {
73+
i.v = l.elems.Remove(e).(*item).v
74+
l.elems.PushFront(i)
75+
return
76+
}
77+
}
78+
79+
// check if cache is full
80+
if l.size+1 > l.cap {
81+
l.elems.Remove(l.elems.Back())
82+
}
83+
l.elems.PushFront(i)
84+
l.size++
85+
return
86+
}

cache_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package main
2+
3+
import (
4+
"math/rand"
5+
"testing"
6+
)
7+
8+
func TestLRU(t *testing.T) {
9+
l := newLRU(false)
10+
l.cap = 2 // for testing
11+
12+
if _, ok := l.Get("a"); ok {
13+
t.Fatalf("Get value from empty LRU")
14+
}
15+
16+
l.Put("a", "1") // a
17+
v, ok := l.Get("a")
18+
if !ok { // a -> b
19+
t.Fatalf("Get value from LRU found nothing")
20+
}
21+
22+
l.Put("b", "2") // b -> a
23+
v, ok = l.Get("a")
24+
if !ok { // a -> b
25+
t.Fatalf("Get value after Put from LRU found nothing")
26+
}
27+
if v != "1" {
28+
t.Fatalf("Get value from LRU want 1 got %v", v)
29+
}
30+
l.Put("c", "3") // c -> a
31+
_, ok = l.Get("b")
32+
if ok {
33+
t.Fatalf("Get value success meaning LRU incorrect")
34+
}
35+
v, ok = l.Get("c")
36+
if !ok {
37+
t.Fatalf("Get value fail meaning LRU incorrect")
38+
}
39+
if v != "3" {
40+
t.Fatalf("Get value from LRU want 3 got %v", v)
41+
}
42+
}
43+
44+
func rands() string {
45+
var alphabet = "qazwsxedcrfvtgbyhnujmikolpQAZWSXEDCRFVTGBYHNUJMIKOLP"
46+
ret := make([]byte, 5)
47+
for i := 0; i < 5; i++ {
48+
ret[i] = alphabet[rand.Intn(len(alphabet))]
49+
}
50+
return BytesToString(ret)
51+
}
52+
53+
func BenchmarkLRU(b *testing.B) {
54+
l := newLRU(false)
55+
l.Put("a", "1")
56+
b.Run("Get", func(b *testing.B) {
57+
b.RunParallel(func(pb *testing.PB) {
58+
for pb.Next() {
59+
l.Get("a")
60+
}
61+
})
62+
})
63+
b.Run("Put-Same", func(b *testing.B) {
64+
b.RunParallel(func(pb *testing.PB) {
65+
// each goroutine put its own k/v
66+
k, v := rands(), rands()
67+
for pb.Next() {
68+
l.Put(k, v)
69+
}
70+
})
71+
})
72+
73+
// This is a very naive bench test, especially it
74+
// mostly measures the rands().
75+
b.Run("Put-Different", func(b *testing.B) {
76+
b.RunParallel(func(pb *testing.PB) {
77+
for pb.Next() {
78+
// each put has a different k/v
79+
l.Put(rands(), rands())
80+
}
81+
})
82+
})
83+
}

config.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@ var (
2121
)
2222

2323
type config struct {
24-
Host string `yaml:"host"`
25-
Addr string `yaml:"addr"`
26-
Store string `yaml:"store"`
27-
Log string `yaml:"log"`
28-
S struct {
24+
Host string `yaml:"host"`
25+
Addr string `yaml:"addr"`
26+
Store string `yaml:"store"`
27+
BackupMin int `yaml:"backup_min"`
28+
BackupDir string `yaml:"backup_dir"`
29+
Log string `yaml:"log"`
30+
S struct {
2931
Prefix string `yaml:"prefix"`
3032
} `yaml:"s"`
3133
X struct {

config.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
host: https://golang.design
77
addr: :8080
88
store: redis://localhost:6379/9
9+
backup_min: 10
10+
backup_dir: ./data/backup
911
log: "golang.design/redir: "
1012
s:
1113
prefix: /s/

data/container.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
host: https://golang.design/
77
addr: :8080
88
store: redis://redis:6379/9
9+
backup_min: 1440 # 24h
10+
backup_dir: ./data/backup
911
log: "golang.design/redir: "
1012
s:
1113
prefix: /s/

db_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ func check(ctx context.Context, t *testing.T, s *store, key string, rr interface
4747
if err != nil {
4848
t.Fatalf("Fetch failure, err: %v\n", err)
4949
}
50-
err = json.Unmarshal([]byte(r), rr)
50+
err = json.Unmarshal(StringToBytes(r), rr)
5151
if err != nil {
5252
t.Fatalf("Unmarshal failure, err: %v\n", err)
5353
}

docker/docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ services:
2424
container_name: redis
2525
restart: always
2626
volumes:
27-
- ../data:/data
27+
- ../data/redis:/data
2828
image: redis:3.2
2929
ports:
3030
- "6379:6379"

handler.go

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,15 @@ package main
77
import (
88
"context"
99
"encoding/json"
10+
"fmt"
1011
"html/template"
12+
"io/ioutil"
1113
"log"
1214
"net/http"
15+
"os"
16+
"time"
17+
18+
"gopkg.in/yaml.v3"
1319
)
1420

1521
type visit struct {
@@ -18,7 +24,8 @@ type visit struct {
1824
}
1925

2026
type server struct {
21-
db *store // *store
27+
db *store
28+
cache *lru
2229
visitCh chan visit
2330
}
2431

@@ -37,6 +44,7 @@ func newServer(ctx context.Context) *server {
3744
}
3845
s := &server{
3946
db: db,
47+
cache: newLRU(true),
4048
visitCh: make(chan visit, 100),
4149
}
4250
go s.counting(ctx)
@@ -69,8 +77,59 @@ func (s *server) registerHandler() {
6977
http.Handle(conf.X.Prefix, s.xHandler(conf.X.VCS, conf.X.ImportPath, conf.X.RepoPath))
7078
}
7179

72-
// backup tries to backup the data store to local files every week.
73-
// it will keeps the latest 10 backups of the data read from data store.
80+
// backup tries to backup the data store to local files.
7481
func (s *server) backup(ctx context.Context) {
75-
// TODO: do self-backups
82+
if _, err := os.Stat(conf.BackupDir); os.IsNotExist(err) {
83+
err := os.Mkdir(conf.BackupDir, os.ModePerm)
84+
if err != nil {
85+
log.Fatalf("cannot create backup directory, err: %v\n", err)
86+
}
87+
}
88+
89+
t := time.NewTicker(time.Minute * time.Duration(conf.BackupMin))
90+
log.Printf("internal backup is running...")
91+
for {
92+
select {
93+
case <-t.C:
94+
r, err := s.db.Keys(ctx, "*")
95+
if err != nil {
96+
log.Printf("backup failure, err: %v\n", err)
97+
continue
98+
}
99+
if len(r) == 0 { // no keys for backup
100+
continue
101+
}
102+
103+
d := make(map[string]interface{}, len(r))
104+
for _, k := range r {
105+
v, err := s.db.Fetch(ctx, k)
106+
if err != nil {
107+
log.Printf("backup failed because of key %v, err: %v\n", k, err)
108+
continue
109+
}
110+
var vv interface{}
111+
err = json.Unmarshal(StringToBytes(v), &vv)
112+
if err != nil {
113+
log.Printf("backup failed because unmarshal of key %v, err: %v\n", k, err)
114+
continue
115+
}
116+
d[k] = vv
117+
}
118+
119+
b, err := yaml.Marshal(d)
120+
if err != nil {
121+
log.Printf("backup failed when converting to yaml, err: %v\n", err)
122+
continue
123+
}
124+
125+
name := fmt.Sprintf("/backup-%s.yml", time.Now().Format(time.RFC3339))
126+
err = ioutil.WriteFile(conf.BackupDir+name, b, os.ModePerm)
127+
if err != nil {
128+
log.Printf("backup failed when saving the file, err: %v\n", err)
129+
continue
130+
}
131+
case <-ctx.Done():
132+
return
133+
}
134+
}
76135
}

short.go

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -105,22 +105,36 @@ func (s *server) sHandler(w http.ResponseWriter, r *http.Request) {
105105
return
106106
}
107107

108-
// TODO: use LRU to optimize fetch speed in the future.
109-
raw, err := s.db.FetchAlias(ctx, alias)
110-
if err != nil {
111-
return
108+
checkdb := func(url string) (string, error) {
109+
raw, err := s.db.FetchAlias(ctx, alias)
110+
if err != nil {
111+
return "", err
112+
}
113+
c := arecord{}
114+
err = json.Unmarshal(StringToBytes(raw), &c)
115+
if err != nil {
116+
return "", err
117+
}
118+
if url != c.URL {
119+
s.cache.Put(alias, c.URL)
120+
url = c.URL
121+
}
122+
return url, nil
112123
}
113-
c := arecord{}
114-
err = json.Unmarshal([]byte(raw), &c)
115-
if err != nil {
116-
return
124+
125+
url, ok := s.cache.Get(alias)
126+
if !ok {
127+
url, err = checkdb(url)
128+
if err != nil {
129+
return
130+
}
117131
}
118132

119133
// redirect the user immediate, but run pv/uv count in background
120-
http.Redirect(w, r, c.URL, http.StatusTemporaryRedirect)
134+
http.Redirect(w, r, url, http.StatusTemporaryRedirect)
121135

122-
// count after the forwarding
123-
s.visitCh <- visit{s.readIP(r), alias}
136+
// count visit in another goroutine so it won't block the redirect.
137+
go func() { s.visitCh <- visit{s.readIP(r), alias} }()
124138
}
125139

126140
// readIP implements a best effort approach to return the real client IP,
@@ -165,7 +179,7 @@ func (s *server) stats(ctx context.Context, w http.ResponseWriter) (retErr error
165179
retErr = err
166180
return
167181
}
168-
err = json.Unmarshal([]byte(raw), &ars.Records[i])
182+
err = json.Unmarshal(StringToBytes(raw), &ars.Records[i])
169183
if err != nil {
170184
retErr = err
171185
return

0 commit comments

Comments
 (0)