├── locmem_test.go ├── .travis.yml ├── .gitignore ├── LICENSE ├── redis_test.go ├── cache.go ├── cache_test.go ├── README.md ├── locmem.go └── redis.go /locmem_test.go: -------------------------------------------------------------------------------- 1 | package gocacher 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.4 5 | services: 6 | - redis-server 7 | before_install: 8 | - go get github.com/axw/gocov/gocov 9 | - go get github.com/mattn/goveralls 10 | - if ! go get code.google.com/p/go.tools/cmd/cover; then go get golang.org/x/tools/cmd/cover; fi 11 | script: 12 | - $HOME/gopath/bin/goveralls -service=travis-ci 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 phonkee 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /redis_test.go: -------------------------------------------------------------------------------- 1 | package gocacher 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/garyburd/redigo/redis" 8 | . "github.com/smartystreets/goconvey/convey" 9 | ) 10 | 11 | func TestRedisSettings(t *testing.T) { 12 | 13 | Convey("Test open/openconnection", t, func() { 14 | driver := &RedisDriver{} 15 | _, err := driver.Open("%://") 16 | So(err, ShouldNotBeNil) 17 | 18 | pool := &redis.Pool{ 19 | Dial: func() (redis.Conn, error) { 20 | return redis.Dial("tcp", "localhost:5379") 21 | }, 22 | } 23 | 24 | _, errOpen := driver.OpenConnection(pool, "expiration=10000") 25 | So(errOpen, ShouldBeNil) 26 | 27 | _, errOpen = driver.OpenConnection(pool, "%") 28 | So(errOpen, ShouldNotBeNil) 29 | 30 | _, errOpen = driver.OpenConnection(pool) 31 | So(errOpen, ShouldBeNil) 32 | }) 33 | 34 | Convey("Test parse query settings", t, func() { 35 | var s *RedisSettings 36 | var err error 37 | 38 | // settings 39 | s, err = NewRedisSettingsFromQuery("pool_max_active=100&pool_max_idle=200&pool_idle_timeout=555&expiration=456&prefix=gocacher") 40 | So(err, ShouldBeNil) 41 | So(s.pool.MaxActive, ShouldEqual, 100) 42 | So(s.pool.MaxIdle, ShouldEqual, 200) 43 | So(s.pool.IdleTimeout, ShouldEqual, 555*time.Second) 44 | So(s.expiration, ShouldEqual, time.Second*456) 45 | So(s.Prefixed("str"), ShouldEqual, "gocacher:str") 46 | 47 | // default settings 48 | s, err = NewRedisSettingsFromQuery("") 49 | So(err, ShouldBeNil) 50 | So(s.pool.IdleTimeout, ShouldEqual, DEFAULT_POOL_IDLE_TIMEOUT) 51 | So(s.pool.MaxActive, ShouldEqual, DEFAULT_POOL_MAX_ACTIVE) 52 | So(s.pool.MaxIdle, ShouldEqual, DEFAULT_POOL_MAX_IDLE) 53 | So(s.expiration, ShouldEqual, DEFAULT_EXPIRATION) 54 | 55 | }) 56 | 57 | Convey("Parse redis dsn", t, func() { 58 | var ( 59 | p *RedisDSN 60 | err error 61 | ) 62 | p, err = ParseRedisDSN("redis://guest:pass@localhost:6379/0") 63 | So(err, ShouldBeNil) 64 | So(p.Database(), ShouldEqual, 0) 65 | So(p.Password(), ShouldEqual, "pass") 66 | So(p.Host(), ShouldEqual, "localhost:6379") 67 | 68 | _, err = ParseRedisDSN("%://") 69 | So(err, ShouldNotBeNil) 70 | 71 | p, _ = ParseRedisDSN("redis://localhost:63/baddb") 72 | So(p.Database(), ShouldEqual, -1) 73 | So(p.Password(), ShouldEqual, "") 74 | 75 | pool := p.Pool() 76 | conn := pool.Get() 77 | 78 | _, err = conn.Do("PING") 79 | So(err, ShouldNotBeNil) 80 | 81 | p, _ = ParseRedisDSN("redis://localhost:6379/123123123") 82 | pool = p.Pool() 83 | conn = pool.Get() 84 | 85 | _, err = conn.Do("PING") 86 | So(err, ShouldNotBeNil) 87 | 88 | }) 89 | 90 | } 91 | -------------------------------------------------------------------------------- /cache.go: -------------------------------------------------------------------------------- 1 | package gocacher 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "time" 7 | 8 | "github.com/phonkee/godsn" 9 | ) 10 | 11 | /* 12 | CacheDriverer interface 13 | All cache drivers must satisfy this interface. 14 | */ 15 | type CacheDriverer interface { 16 | Open(dsn string) (Cacher, error) 17 | 18 | OpenConnection(connection interface{}, settings ...string) (Cacher, error) 19 | } 20 | 21 | /* 22 | Cache interface 23 | All cache implementations must satisfy this interface. 24 | */ 25 | type Cacher interface { 26 | // returns cache value by key 27 | Get(key string) ([]byte, error) 28 | 29 | // Sets key to value, if expiration is not given it's used from settings 30 | Set(key string, value []byte, expiration ...time.Duration) error 31 | 32 | // Deletes key in cache 33 | Delete(key string) error 34 | 35 | // Increments key by 1, if num is given by that amout will be incremented 36 | Incr(key string, num ...int) (int, error) 37 | 38 | // Decrements key by 1, if num is given it decrements by given number 39 | Decr(key string, num ...int) (int, error) 40 | 41 | // Return cache to cache pool 42 | Close() error 43 | } 44 | 45 | // registered cache drivers 46 | var drivers = make(map[string]CacheDriverer) 47 | 48 | /* 49 | Register makes a cache driver available by the provided name. 50 | If Register is called twice with the same name or if driver is nil, 51 | it panics. 52 | */ 53 | func Register(name string, driver CacheDriverer) { 54 | if driver == nil { 55 | panic("gocacher: Register driver is nil") 56 | } 57 | if _, dup := drivers[name]; dup { 58 | panic("gocacher: Register called twice for driver " + name) 59 | } 60 | drivers[name] = driver 61 | } 62 | 63 | // returns list of available driver names 64 | func Drivers() []string { 65 | var list []string 66 | for name := range drivers { 67 | list = append(list, name) 68 | } 69 | sort.Strings(list) 70 | return list 71 | } 72 | 73 | /* 74 | Opens cache by dsn 75 | This method is preferred to use for instantiate Cache. 76 | DSN is used as connector. Additional settings are set as url parameters. 77 | 78 | Example: 79 | redis://localhost:5379/0?pool_max_active=10&expiration=10 80 | 81 | Which translates to connection to localhost:5379 redis pool max_active=10 82 | and expiration for cache.Set default value will be set to 10 seconds 83 | 84 | */ 85 | func Open(dsn string) (Cacher, error) { 86 | d, err := godsn.Parse(dsn) 87 | if err != nil { 88 | return nil, err 89 | } 90 | di, ok := drivers[d.Scheme()] 91 | if !ok { 92 | return nil, fmt.Errorf("gocacher: unknown driver %q (forgotten import?)", d.Scheme()) 93 | } 94 | 95 | return di.Open(dsn) 96 | } 97 | 98 | // opens message queue by name and connection 99 | func OpenConnection(driver string, connection interface{}, settings ...string) (Cacher, error) { 100 | di, ok := drivers[driver] 101 | if !ok { 102 | return nil, fmt.Errorf("gocacher: unknown driver %q (forgotten import?)", driver) 103 | } 104 | // additional settings 105 | urlSettings := "" 106 | if len(settings) > 0 { 107 | urlSettings = settings[0] 108 | } 109 | 110 | return di.OpenConnection(connection, urlSettings) 111 | } 112 | -------------------------------------------------------------------------------- /cache_test.go: -------------------------------------------------------------------------------- 1 | package gocacher 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/garyburd/redigo/redis" 8 | . "github.com/smartystreets/goconvey/convey" 9 | ) 10 | 11 | func TestCache(t *testing.T) { 12 | Convey("Test register cache driver.", t, func() { 13 | driver := &RedisDriver{} 14 | So(func() { Register("new", driver) }, ShouldNotPanic) 15 | So(func() { Register("new", driver) }, ShouldPanic) 16 | So(func() { Register("bad", nil) }, ShouldPanic) 17 | So(Drivers(), ShouldContain, "new") 18 | }) 19 | 20 | Convey("Test Open cache.", t, func() { 21 | _, err := Open("nonexisting://") 22 | So(err, ShouldNotBeNil) 23 | 24 | _, errOC := OpenConnection("redis", nil) 25 | So(errOC, ShouldNotBeNil) 26 | 27 | _, err = Open("%://") 28 | So(err, ShouldNotBeNil) 29 | 30 | _, err = OpenConnection("imap", nil) 31 | So(err, ShouldNotBeNil) 32 | 33 | pool := &redis.Pool{ 34 | Dial: func() (redis.Conn, error) { 35 | return redis.Dial("tcp", "localhost:5379") 36 | }, 37 | } 38 | 39 | _, err = OpenConnection("redis", pool, "expiration=100") 40 | So(err, ShouldBeNil) 41 | }) 42 | } 43 | 44 | func TestCacheCommands(t *testing.T) { 45 | 46 | dsns := []string{ 47 | "redis://localhost:6379/4", 48 | "locmem:///database?gc=true", 49 | } 50 | 51 | Convey("Test cache set/get.", t, func() { 52 | for _, dsn := range dsns { 53 | 54 | testKey := "key" 55 | testMessage := []byte("test message") 56 | 57 | cache, err := Open(dsn) 58 | So(err, ShouldBeNil) 59 | 60 | errSet := cache.Set(testKey, testMessage) 61 | So(errSet, ShouldBeNil) 62 | 63 | value, errGet := cache.Get(testKey) 64 | So(errGet, ShouldBeNil) 65 | So(value, ShouldResemble, testMessage) 66 | 67 | _, errGetNonExisting := cache.Get("non_existing") 68 | So(errGetNonExisting, ShouldNotBeNil) 69 | 70 | testExpiryKey := "test-expiry-key" 71 | 72 | errSet = cache.Set(testExpiryKey, testMessage, time.Second*1) 73 | So(errSet, ShouldBeNil) 74 | 75 | time.Sleep(time.Second * 1) 76 | 77 | _, errGet = cache.Get(testExpiryKey) 78 | So(errGet, ShouldNotBeNil) 79 | 80 | } 81 | }) 82 | 83 | Convey("Test cache delete.", t, func() { 84 | for _, dsn := range dsns { 85 | cache, errOpen := Open(dsn) 86 | So(errOpen, ShouldBeNil) 87 | 88 | testKey := "key-delete" 89 | testMessage := []byte("test message") 90 | 91 | _, err := cache.Get(testKey) 92 | So(err, ShouldNotBeNil) 93 | 94 | errSet := cache.Set(testKey, testMessage) 95 | So(errSet, ShouldBeNil) 96 | 97 | _, errGet := cache.Get(testKey) 98 | So(errGet, ShouldBeNil) 99 | 100 | errDelete := cache.Delete(testKey) 101 | So(errDelete, ShouldBeNil) 102 | 103 | _, errGetDeleted := cache.Get(testKey) 104 | So(errGetDeleted, ShouldNotBeNil) 105 | 106 | // close cache 107 | So(cache.Close(), ShouldBeNil) 108 | } 109 | }) 110 | 111 | Convey("Test cache incr/decr.", t, func() { 112 | for _, dsn := range dsns { 113 | cache, errOpen := Open(dsn) 114 | So(errOpen, ShouldBeNil) 115 | 116 | testKey := "incr_key" 117 | 118 | // delete before test 119 | _ = cache.Delete(testKey) 120 | 121 | i, errIncr := cache.Incr(testKey) 122 | So(errIncr, ShouldBeNil) 123 | So(i, ShouldEqual, 1) 124 | 125 | i, errIncr = cache.Incr(testKey, 10) 126 | So(errIncr, ShouldBeNil) 127 | So(i, ShouldEqual, 11) 128 | 129 | var errDecr error 130 | i, errDecr = cache.Decr(testKey) 131 | So(errDecr, ShouldBeNil) 132 | So(i, ShouldEqual, 10) 133 | 134 | i, errDecr = cache.Decr(testKey, 5) 135 | So(errDecr, ShouldBeNil) 136 | So(i, ShouldEqual, 5) 137 | 138 | // close cache 139 | So(cache.Close(), ShouldBeNil) 140 | } 141 | }) 142 | 143 | } 144 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gocacher 2 | 3 | [![Build Status](https://travis-ci.org/phonkee/gocacher.svg?branch=master)](https://travis-ci.org/phonkee/gocacher) 4 | [![Coverage Status](https://coveralls.io/repos/phonkee/gocacher/badge.svg)](https://coveralls.io/r/phonkee/gocacher) 5 | 6 | 7 | Gocacher is cache abstraction. It's intended to use in web applications with 8 | possibility to choose cache implementation directly from configuration (dsn). 9 | 10 | Currently redis implementation is written and tested, but in the future there will 11 | be more implementations (memcached, and other..). 12 | 13 | All cache implementations satisfy this interface 14 | ```go 15 | type Cacher interface { 16 | // returns cache value by key 17 | Get(key string) ([]byte, error) 18 | 19 | // Sets key to value, if expiration is not given it's used from settings 20 | Set(key string, value []byte, expiration ...time.Duration) error 21 | 22 | // Deletes key in cache 23 | Delete(key string) error 24 | 25 | // Increments key by 1, if num is given by that amout will be incremented 26 | Incr(key string, num ...int) (int, error) 27 | 28 | // Decrements key by 1, if num is given it decrements by given number 29 | Decr(key string, num ...int) (int, error) 30 | 31 | // Return cache to cache pool 32 | Close() error 33 | } 34 | ``` 35 | 36 | 37 | Before we dive into examples all use this import 38 | ```go 39 | import ( 40 | "time" 41 | "github.com/phonkee/gocacher" 42 | ) 43 | ``` 44 | 45 | Examples: 46 | 47 | ```go 48 | // Open Cache with expiration of 60 seconds and prefix "cache" 49 | cache, _ := gocacher.Open("redis://localhost:5379/1?expiration=60&prefix=cache") 50 | 51 | // this key will be set with expiration set in url "expiration=60" 52 | cache.Set("key", []byte("message")) 53 | 54 | // value will be []byte("message") 55 | value, _ := cache.Get("key") 56 | 57 | // this key will expire in 5 seconds 58 | cache.Set("key-with-expiration", []byte("message"), time.Seconds*5) 59 | 60 | // deletes the key from cache 61 | cache.Delete("key") 62 | 63 | // increments key and returns incremented value 64 | i, _ := cache.Incr("increment-key") // i is now 1 65 | i, _ := cache.Incr("increment-key", 10) // i is now 11 66 | 67 | i, _ := cache.Decr("increment-key", 5) // i is now 6 68 | i, _ := cache.Decr("increment-key") // i is now 5 69 | 70 | ``` 71 | 72 | ### Open Cache connection 73 | 74 | You can open connection in two ways. 75 | 76 | ```go 77 | cache, _ := gocacher.Open("redis://localhost:5379/1") 78 | ``` 79 | 80 | or you can provide connection to gocacher.OpenConnection and provide additional settings 81 | as url query 82 | 83 | ```go 84 | pool := &redis.Pool{ 85 | Dial: func() (redis.Conn, error) { 86 | return redis.Dial("tcp", "localhost:5379") 87 | }, 88 | } 89 | 90 | cache, _ := gocacher.OpenConnection("redis", pool) 91 | 92 | // With additional cache settings 93 | cache, _ := gocacher.OpenConnection("redis", pool, "expiration=60&prefix=cache") 94 | ``` 95 | 96 | #### Parameters 97 | You can pass multiple parameters to `Open` or `OpenConnection` as query part of dsn. 98 | 99 | e.g. 100 | ```go 101 | cache, _ := gocacher.Open("locmem:///0?expiration=10s") 102 | cache, _ := gocacher.Open("redis://localhost:5379/1?expiration=60&prefix=cache") 103 | ``` 104 | 105 | ### Cache implementations 106 | 107 | Currently two imlpementations are available: 108 | * redis - redis storage 109 | * locmem - local memory storage 110 | 111 | 112 | #### Locmem 113 | Local memory cache. Supports multiple databases. Currently there is no garbage collect or limiting of items in database. 114 | This will be updated in near future. 115 | 116 | ```go 117 | cache, _ := gocacher.Open("locmem:///0") 118 | ``` 119 | 120 | ##### parameters: 121 | * expiration - string parsed with time parse duration. (default expiration is 0s - neverending) 122 | 123 | #### Redis 124 | Redis cache support. Supports multiple databases and all commands. 125 | 126 | ##### parameters: 127 | * pool_max_active - redis pool settings 128 | * pool_max_idle - redis pool settings 129 | * pool_idle_timeout - redis pool settings 130 | * expiration - default expiration 131 | * prefix - prefix for cache keys 132 | 133 | ## Contribute 134 | Welcome! 135 | -------------------------------------------------------------------------------- /locmem.go: -------------------------------------------------------------------------------- 1 | package gocacher 2 | 3 | import ( 4 | "errors" 5 | 6 | "time" 7 | 8 | "sync" 9 | 10 | "strconv" 11 | 12 | "strings" 13 | 14 | "github.com/phonkee/godsn" 15 | ) 16 | 17 | const ( 18 | LOCMEM_DEFAULT_DATABASE = "default" 19 | ) 20 | 21 | var ( 22 | ErrNotFound = errors.New("locmem item not found") 23 | 24 | // storage where all databases are stored 25 | storage map[string]map[string]*locmemCacheItem 26 | 27 | // mutex to lock storage for concurrent access 28 | mutex *sync.RWMutex 29 | ) 30 | 31 | func init() { 32 | Register("locmem", &locmemDriver{}) 33 | 34 | // Instantiate blank storage 35 | storage = map[string]map[string]*locmemCacheItem{} 36 | mutex = &sync.RWMutex{} 37 | } 38 | 39 | /* 40 | locmemDriver - implementation of local memory cache. 41 | 42 | It provides extended functionality such as garbage collector 43 | */ 44 | type locmemDriver struct { 45 | } 46 | 47 | func (l *locmemDriver) Open(dsn string) (cacher Cacher, err error) { 48 | 49 | if settings, errSettings := newLocmemSettings(dsn); errSettings != nil { 50 | err = errSettings 51 | return 52 | } else { 53 | cacher = newLocmemCache(settings) 54 | } 55 | return 56 | } 57 | 58 | func (l *locmemDriver) OpenConnection(connection interface{}, settings ...string) (cacher Cacher, err error) { 59 | panic("doesn't make sense for locmem cache") 60 | return 61 | } 62 | 63 | /* 64 | locmemCache 65 | */ 66 | 67 | func newLocmemCache(settings *locmemSettings) Cacher { 68 | return &locmemCache{ 69 | settings: settings, 70 | } 71 | } 72 | 73 | type locmemCache struct { 74 | settings *locmemSettings 75 | } 76 | 77 | /* 78 | Get returns cache value if found 79 | */ 80 | func (l *locmemCache) Get(key string) (result []byte, err error) { 81 | 82 | hasExpired := false 83 | 84 | db := getDatabase(l.settings.database) 85 | 86 | func() { 87 | mutex.RLock() 88 | defer mutex.RUnlock() 89 | 90 | if item, ok := db[key]; ok { 91 | // cache item found 92 | if item.expiration.IsZero() { 93 | result = item.value 94 | } else { 95 | if time.Now().Before(item.expiration) { 96 | result = item.value 97 | } else { 98 | // delete since expired 99 | err = ErrNotFound 100 | hasExpired = true 101 | } 102 | } 103 | } else { 104 | // cache item not found 105 | err = ErrNotFound 106 | } 107 | 108 | }() 109 | 110 | // item has expired - delete it 111 | if hasExpired { 112 | l.Delete(key) 113 | } 114 | 115 | return 116 | } 117 | 118 | func (l *locmemCache) Set(key string, value []byte, expiration ...time.Duration) (err error) { 119 | db := getDatabase(l.settings.database) 120 | 121 | mutex.Lock() 122 | defer mutex.Unlock() 123 | 124 | // expiration value to cache item 125 | 126 | expval := time.Time{} 127 | 128 | if len(expiration) == 1 { 129 | expval = time.Now().Add(expiration[0]) 130 | } else if len(expiration) > 1 { 131 | panic("set multiple expirations in one call") 132 | } 133 | 134 | db[key] = &locmemCacheItem{ 135 | expiration: expval, 136 | value: value, 137 | } 138 | 139 | return 140 | } 141 | 142 | // Deletes key in cache 143 | func (l *locmemCache) Delete(key string) (err error) { 144 | 145 | db := getDatabase(l.settings.database) 146 | 147 | mutex.Lock() 148 | defer mutex.Unlock() 149 | 150 | if _, ok := db[key]; ok { 151 | // delete from cache if exists 152 | delete(db, key) 153 | } else { 154 | err = ErrNotFound 155 | } 156 | 157 | return 158 | } 159 | 160 | // Incr increments key either by 1, or if num is given by that amout will be incremented 161 | func (l *locmemCache) Incr(key string, num ...int) (result int, err error) { 162 | db := getDatabase(l.settings.database) 163 | 164 | mutex.Lock() 165 | defer mutex.Unlock() 166 | 167 | if len(num) > 0 { 168 | result = num[0] 169 | } else { 170 | result = 1 171 | 172 | } 173 | 174 | expiration := time.Time{} 175 | 176 | if item, ok := db[key]; ok { 177 | 178 | if numValue, errParse := strconv.Atoi(string(item.value)); errParse == nil { 179 | result = numValue + result 180 | } 181 | } 182 | 183 | item := &locmemCacheItem{ 184 | expiration: expiration, 185 | value: []byte(strconv.Itoa(result)), 186 | } 187 | db[key] = item 188 | 189 | return 190 | } 191 | 192 | // Decrements key by 1, if num is given it decrements by given number 193 | func (l *locmemCache) Decr(key string, num ...int) (result int, err error) { 194 | db := getDatabase(l.settings.database) 195 | 196 | mutex.Lock() 197 | defer mutex.Unlock() 198 | 199 | if len(num) > 0 { 200 | result = num[0] 201 | } else { 202 | result = 1 203 | 204 | } 205 | 206 | // zero expiration for incr/decr 207 | expiration := time.Time{} 208 | 209 | if item, ok := db[key]; ok { 210 | 211 | if numValue, errParse := strconv.Atoi(string(item.value)); errParse == nil { 212 | result = numValue - result 213 | } 214 | } 215 | 216 | item := &locmemCacheItem{ 217 | expiration: expiration, 218 | value: []byte(strconv.Itoa(result)), 219 | } 220 | db[key] = item 221 | 222 | return 223 | } 224 | 225 | // Return cache to cache pool 226 | func (l *locmemCache) Close() (err error) { 227 | /* 228 | Here we should stop garbage collect if it was running 229 | */ 230 | return 231 | } 232 | 233 | /* 234 | cache Item 235 | */ 236 | type locmemCacheItem struct { 237 | expiration time.Time 238 | value []byte 239 | } 240 | 241 | func (l *locmemCacheItem) IsValid(t time.Time) bool { 242 | 243 | if l.expiration.IsZero() { 244 | return true 245 | } 246 | 247 | return t.Before(l.expiration) 248 | } 249 | 250 | /* 251 | locmemSettings 252 | */ 253 | type locmemSettings struct { 254 | 255 | // database name 256 | database string 257 | 258 | // enable garbage collect of old cache items 259 | expiration time.Duration 260 | } 261 | 262 | func newLocmemSettings(dsn string) (settings *locmemSettings, err error) { 263 | settings = &locmemSettings{} 264 | 265 | if values, errParse := godsn.Parse(dsn); err != nil { 266 | return nil, errParse 267 | } else { 268 | 269 | exp_param := values.GetString(URL_PARAM_EXPIRATION, "") 270 | if exp_param == "" { 271 | settings.expiration = DEFAULT_EXPIRATION 272 | } else { 273 | if settings.expiration, err = time.ParseDuration(exp_param); err != nil { 274 | return 275 | } 276 | 277 | } 278 | 279 | // set database name 280 | settings.database = strings.Trim(strings.TrimSpace(values.Path()), "/") 281 | if settings.database == "" { 282 | settings.database = LOCMEM_DEFAULT_DATABASE 283 | } 284 | } 285 | 286 | return 287 | } 288 | 289 | /* 290 | Storage utilities 291 | */ 292 | func getDatabase(database string) (result map[string]*locmemCacheItem) { 293 | mutex.Lock() 294 | defer mutex.Unlock() 295 | 296 | var ok bool 297 | 298 | if result, ok = storage[database]; !ok { 299 | storage[database] = map[string]*locmemCacheItem{} 300 | result = storage[database] 301 | } 302 | 303 | return 304 | } 305 | -------------------------------------------------------------------------------- /redis.go: -------------------------------------------------------------------------------- 1 | package gocacher 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | "time" 8 | 9 | "github.com/garyburd/redigo/redis" 10 | "github.com/phonkee/godsn" 11 | ) 12 | 13 | const ( 14 | URL_PARAM_POOL_MAX_ACTIVE = "pool_max_active" 15 | URL_PARAM_POOL_MAX_IDLE = "pool_max_idle" 16 | URL_PARAM_POOL_IDLE_TIMEOUT = "pool_idle_timeout" 17 | URL_PARAM_EXPIRATION = "expiration" 18 | URL_PARAM_PREFIX = "prefix" 19 | 20 | DEFAULT_POOL_MAX_ACTIVE = 20 21 | DEFAULT_POOL_MAX_IDLE = 10 22 | DEFAULT_POOL_IDLE_TIMEOUT = 200 * time.Millisecond 23 | DEFAULT_EXPIRATION = 0 * time.Second 24 | DEFAULT_PREFIX = "" 25 | ) 26 | 27 | func init() { 28 | Register("redis", &RedisDriver{}) 29 | } 30 | 31 | type RedisDriver struct{} 32 | 33 | func (r *RedisDriver) Open(dsn string) (Cacher, error) { 34 | d, err := ParseRedisDSN(dsn) 35 | if err != nil { 36 | return nil, err 37 | } 38 | cache := RedisCache{ 39 | pool: d.Pool(), 40 | expiration: d.settings.expiration, 41 | settings: d.settings, 42 | } 43 | 44 | return &cache, nil 45 | } 46 | 47 | func (r *RedisDriver) OpenConnection(connection interface{}, settings ...string) (Cacher, error) { 48 | 49 | switch connection.(type) { 50 | case *redis.Pool: 51 | break 52 | default: 53 | return nil, fmt.Errorf("Connection %s is unknown.", connection) 54 | } 55 | 56 | sett := "" 57 | if len(settings) > 0 { 58 | sett = settings[0] 59 | } 60 | 61 | s, err := NewRedisSettingsFromQuery(sett) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | cache := RedisCache{ 67 | pool: connection.(*redis.Pool), 68 | expiration: s.expiration, 69 | settings: s, 70 | } 71 | 72 | return &cache, nil 73 | } 74 | 75 | type PoolSettings struct { 76 | MaxIdle int 77 | MaxActive int 78 | IdleTimeout time.Duration 79 | } 80 | 81 | type RedisSettings struct { 82 | pool PoolSettings 83 | expiration time.Duration 84 | prefix string 85 | } 86 | 87 | // returns prefixed string 88 | func (r *RedisSettings) Prefixed(str string) string { 89 | if r.prefix == "" { 90 | return str 91 | } 92 | return r.prefix + ":" + str 93 | } 94 | 95 | func NewRedisSettings(values *godsn.DSNValues) (*RedisSettings, error) { 96 | settings := RedisSettings{ 97 | pool: PoolSettings{ 98 | MaxIdle: 10, 99 | MaxActive: 20, 100 | IdleTimeout: 200, 101 | }, 102 | expiration: DEFAULT_EXPIRATION, 103 | prefix: DEFAULT_PREFIX, 104 | } 105 | 106 | settings.pool.MaxActive = values.GetInt( 107 | URL_PARAM_POOL_MAX_ACTIVE, 108 | DEFAULT_POOL_MAX_ACTIVE) 109 | 110 | settings.pool.MaxIdle = values.GetInt( 111 | URL_PARAM_POOL_MAX_IDLE, 112 | DEFAULT_POOL_MAX_IDLE) 113 | 114 | settings.pool.IdleTimeout = values.GetSeconds( 115 | URL_PARAM_POOL_IDLE_TIMEOUT, 116 | DEFAULT_POOL_IDLE_TIMEOUT) 117 | 118 | settings.expiration = values.GetSeconds( 119 | URL_PARAM_EXPIRATION, 120 | DEFAULT_EXPIRATION) 121 | 122 | settings.prefix = values.GetString( 123 | URL_PARAM_PREFIX, 124 | DEFAULT_PREFIX) 125 | return &settings, nil 126 | } 127 | 128 | func NewRedisSettingsFromQuery(query string) (*RedisSettings, error) { 129 | values, err := godsn.ParseQuery(query) 130 | if err != nil { 131 | return nil, err 132 | } 133 | 134 | return NewRedisSettings(values) 135 | } 136 | 137 | // redis dsn 138 | type RedisDSN struct { 139 | *godsn.DSN 140 | settings *RedisSettings 141 | } 142 | 143 | func (r *RedisDSN) Database() int { 144 | path := strings.TrimLeft(r.Path(), "/") 145 | if db, err := strconv.Atoi(path); err == nil { 146 | return db 147 | } 148 | return -1 149 | } 150 | 151 | func (r *RedisDSN) Password() string { 152 | if r.User() != nil { 153 | if pass, ok := r.User().Password(); ok { 154 | return pass 155 | } 156 | } 157 | return "" 158 | } 159 | 160 | func ParseRedisDSN(dsn string) (*RedisDSN, error) { 161 | d, err := godsn.Parse(dsn) 162 | if err != nil { 163 | return nil, err 164 | } 165 | // this returns no error 166 | settings, _ := NewRedisSettings(d.DSNValues) 167 | rd := RedisDSN{ 168 | d, 169 | settings, 170 | } 171 | 172 | rd.settings = settings 173 | 174 | return &rd, nil 175 | } 176 | 177 | // returns *redis.Poool 178 | func (r *RedisDSN) Pool() *redis.Pool { 179 | pool := redis.Pool{ 180 | MaxIdle: r.settings.pool.MaxIdle, 181 | MaxActive: r.settings.pool.MaxActive, 182 | IdleTimeout: r.settings.pool.IdleTimeout, 183 | Dial: func() (redis.Conn, error) { 184 | conn, err := redis.Dial("tcp", r.Host()) 185 | if err != nil { 186 | return nil, err 187 | } 188 | if db := r.Database(); db > -1 { 189 | if _, err := conn.Do("SELECT", db); err != nil { 190 | return nil, err 191 | } 192 | } 193 | return conn, nil 194 | }, 195 | TestOnBorrow: func(c redis.Conn, t time.Time) error { 196 | _, err := c.Do("PING") 197 | return err 198 | }, 199 | } 200 | 201 | return &pool 202 | } 203 | 204 | // Redis cache implementation 205 | type RedisCache struct { 206 | pool *redis.Pool 207 | expiration time.Duration 208 | settings *RedisSettings 209 | } 210 | 211 | // returns data from cache 212 | func (r *RedisCache) Get(key string) ([]byte, error) { 213 | conn := r.pool.Get() 214 | defer conn.Close() 215 | 216 | if result, err := redis.Bytes(conn.Do("GET", r.settings.Prefixed(key))); err != nil { 217 | return nil, err 218 | } else { 219 | return result, nil 220 | } 221 | } 222 | 223 | // Sets to cache 224 | func (r *RedisCache) Set(key string, value []byte, expiration ...time.Duration) error { 225 | conn := r.pool.Get() 226 | defer conn.Close() 227 | 228 | d := r.expiration 229 | if len(expiration) > 0 { 230 | d = expiration[0] 231 | } 232 | 233 | args := redis.Args{}.Add(r.settings.Prefixed(key)).Add(value) 234 | ms := int(d / time.Millisecond) 235 | 236 | if ms > 0 { 237 | args = args.Add("PX").Add(ms) 238 | } 239 | 240 | _, err := conn.Do("SET", args...) 241 | return err 242 | } 243 | 244 | // deletes from cache 245 | func (r *RedisCache) Delete(key string) error { 246 | conn := r.pool.Get() 247 | defer conn.Close() 248 | 249 | _, err := conn.Do("DEL", r.settings.Prefixed(key)) 250 | return err 251 | } 252 | 253 | // Increments key in cache 254 | func (r *RedisCache) Incr(key string, num ...int) (int, error) { 255 | conn := r.pool.Get() 256 | defer conn.Close() 257 | 258 | n := 1 259 | if len(num) > 0 { 260 | n = num[0] 261 | } 262 | 263 | return redis.Int(conn.Do("INCRBY", r.settings.Prefixed(key), n)) 264 | } 265 | 266 | // Decrements key in cache 267 | func (r *RedisCache) Decr(key string, num ...int) (int, error) { 268 | conn := r.pool.Get() 269 | defer conn.Close() 270 | 271 | n := 1 272 | if len(num) > 0 { 273 | n = num[0] 274 | } 275 | 276 | return redis.Int(conn.Do("DECRBY", r.settings.Prefixed(key), n)) 277 | } 278 | 279 | // closes cache (it's underlying connection) 280 | func (r *RedisCache) Close() error { 281 | return r.pool.Close() 282 | } 283 | --------------------------------------------------------------------------------