/
simpleredis.go
737 lines (640 loc) · 19.1 KB
/
simpleredis.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
// Package simpleredis provides an easy way to use Redis.
package simpleredis
import (
"errors"
"strconv"
"strings"
"time"
"github.com/gomodule/redigo/redis"
)
const (
// Version number. Stable API within major version numbers.
Version = 2.6
// The default [url]:port that Redis is running at
defaultRedisServer = ":6379"
)
// Common for each of the Redis data structures used here
type redisDatastructure struct {
pool *ConnectionPool
id string
dbindex int
}
type (
// A pool of readily available Redis connections
ConnectionPool redis.Pool
List redisDatastructure
Set redisDatastructure
HashMap redisDatastructure
KeyValue redisDatastructure
)
var (
// Timeout settings for new connections
connectTimeout = 7 * time.Second
readTimeout = 7 * time.Second
writeTimeout = 7 * time.Second
idleTimeout = 240 * time.Second
// How many connections should stay ready for requests, at a maximum?
// When an idle connection is used, new idle connections are created.
maxIdleConnections = 3
)
/* --- Helper functions --- */
// Connect to the local instance of Redis at port 6379
func newRedisConnection() (redis.Conn, error) {
return newRedisConnectionTo(defaultRedisServer)
}
// Connect to host:port, host may be omitted, so ":6379" is valid.
// Will not try to AUTH with any given password (password@host:port).
func newRedisConnectionTo(hostColonPort string) (redis.Conn, error) {
// Discard the password, if provided
if _, theRest, ok := twoFields(hostColonPort, "@"); ok {
hostColonPort = theRest
}
hostColonPort = strings.TrimSpace(hostColonPort)
c, err := redis.Dial("tcp", hostColonPort, redis.DialConnectTimeout(connectTimeout), redis.DialReadTimeout(readTimeout), redis.DialWriteTimeout(writeTimeout))
if err != nil {
if c != nil {
c.Close()
}
return nil, err
}
return c, nil
}
// Get a string from a list of results at a given position
func getString(bi []interface{}, i int) string {
return string(bi[i].([]uint8))
}
// Test if the local Redis server is up and running
func TestConnection() (err error) {
return TestConnectionHost(defaultRedisServer)
}
// Test if a given Redis server at host:port is up and running.
// Does not try to PING or AUTH.
func TestConnectionHost(hostColonPort string) (err error) {
// Connect to the given host:port
conn, err := newRedisConnectionTo(hostColonPort)
defer func() {
if conn != nil {
conn.Close()
}
if r := recover(); r != nil {
err = errors.New("Could not connect to redis server: " + hostColonPort)
}
}()
return err
}
/* --- ConnectionPool functions --- */
func copyPoolValues(src *redis.Pool) ConnectionPool {
return ConnectionPool{
Dial: src.Dial,
DialContext: src.DialContext,
TestOnBorrow: src.TestOnBorrow,
MaxIdle: src.MaxIdle,
MaxActive: src.MaxActive,
IdleTimeout: src.IdleTimeout,
Wait: src.Wait,
MaxConnLifetime: src.MaxConnLifetime,
}
}
// Create a new connection pool
func NewConnectionPool() *ConnectionPool {
// The second argument is the maximum number of idle connections
redisPool := &redis.Pool{
MaxIdle: maxIdleConnections,
IdleTimeout: idleTimeout,
Dial: newRedisConnection,
}
pool := copyPoolValues(redisPool)
return &pool
}
// Split a string into two parts, given a delimiter.
// Returns the two parts and true if it works out.
func twoFields(s, delim string) (string, string, bool) {
if strings.Count(s, delim) != 1 {
return s, "", false
}
fields := strings.Split(s, delim)
return fields[0], fields[1], true
}
// Create a new connection pool given a host:port string.
// A password may be supplied as well, on the form "password@host:port".
func NewConnectionPoolHost(hostColonPort string) *ConnectionPool {
// Create a redis Pool
redisPool := &redis.Pool{
// Maximum number of idle connections to the redis database
MaxIdle: maxIdleConnections,
IdleTimeout: idleTimeout,
// Anonymous function for calling new RedisConnectionTo with the host:port
Dial: func() (redis.Conn, error) {
conn, err := newRedisConnectionTo(hostColonPort)
if err != nil {
return nil, err
}
// If a password is given, use it to authenticate
if password, _, ok := twoFields(hostColonPort, "@"); ok {
if password != "" {
if _, err := conn.Do("AUTH", password); err != nil {
conn.Close()
return nil, err
}
}
}
return conn, err
},
}
pool := copyPoolValues(redisPool)
return &pool
}
// Set the number of maximum *idle* connections standing ready when
// creating new connection pools. When an idle connection is used,
// a new idle connection is created. The default is 3 and should be fine
// for most cases.
func SetMaxIdleConnections(maximum int) {
maxIdleConnections = maximum
}
// Get one of the available connections from the connection pool, given a database index
func (pool *ConnectionPool) Get(dbindex int) redis.Conn {
redisPool := (*redis.Pool)(pool)
conn := redisPool.Get()
// The default database index is 0
if dbindex != 0 {
// SELECT is not critical, ignore the return values
conn.Do("SELECT", strconv.Itoa(dbindex))
}
return conn
}
// Ping the server by sending a PING command
func (pool *ConnectionPool) Ping() error {
redisPool := (*redis.Pool)(pool)
conn := redisPool.Get()
_, err := conn.Do("PING")
return err
}
// Close down the connection pool
func (pool *ConnectionPool) Close() {
redisPool := (*redis.Pool)(pool)
redisPool.Close()
}
/* --- List functions --- */
// Create a new list
func NewList(pool *ConnectionPool, id string) *List {
return &List{pool, id, 0}
}
// Select a different database
func (rl *List) SelectDatabase(dbindex int) {
rl.dbindex = dbindex
}
// Returns the element at index index in the list
func (rl *List) Get(index int64) (string, error) {
conn := rl.pool.Get(rl.dbindex)
result, err := conn.Do("LINDEX", rl.id, index)
if err != nil {
panic(err)
}
return redis.String(result, err)
}
// Get the size of the list
func (rl *List) Size() (int64, error) {
conn := rl.pool.Get(rl.dbindex)
size, err := conn.Do("LLEN", rl.id)
if err != nil {
panic(err)
}
return redis.Int64(size, err)
}
// Removes and returns the first element of the list
func (rl *List) PopFirst() (string, error) {
conn := rl.pool.Get(rl.dbindex)
result, err := conn.Do("LPOP", rl.id)
if err != nil {
panic(err)
}
return redis.String(result, err)
}
// Removes and returns the last element of the list
func (rl *List) PopLast() (string, error) {
conn := rl.pool.Get(rl.dbindex)
result, err := conn.Do("LPOP", rl.id)
if err != nil {
panic(err)
}
return redis.String(result, err)
}
// Add an element to the start of the list
func (rl *List) AddStart(value string) error {
conn := rl.pool.Get(rl.dbindex)
_, err := conn.Do("RPUSH", rl.id, value)
return err
}
// Add an element to the end of the list list
func (rl *List) AddEnd(value string) error {
conn := rl.pool.Get(rl.dbindex)
_, err := conn.Do("LPUSH", rl.id, value)
return err
}
// Default Add, aliased to List.AddStart
func (rl *List) Add(value string) error {
return rl.AddStart(value)
}
// Get all elements of a list
func (rl *List) All() ([]string, error) {
conn := rl.pool.Get(rl.dbindex)
result, err := redis.Values(conn.Do("LRANGE", rl.id, "0", "-1"))
strs := make([]string, len(result))
for i := 0; i < len(result); i++ {
strs[i] = getString(result, i)
}
return strs, err
}
// Deprecated
func (rl *List) GetAll() ([]string, error) {
return rl.All()
}
// Get the last element of a list
func (rl *List) Last() (string, error) {
conn := rl.pool.Get(rl.dbindex)
result, err := redis.Values(conn.Do("LRANGE", rl.id, "-1", "-1"))
if len(result) == 1 {
return getString(result, 0), err
}
return "", err
}
// Deprecated
func (rl *List) GetLast() (string, error) {
return rl.Last()
}
// Get the last N elements of a list
func (rl *List) LastN(n int) ([]string, error) {
conn := rl.pool.Get(rl.dbindex)
result, err := redis.Values(conn.Do("LRANGE", rl.id, "-"+strconv.Itoa(n), "-1"))
strs := make([]string, len(result))
for i := 0; i < len(result); i++ {
strs[i] = getString(result, i)
}
return strs, err
}
// Deprecated
func (rl *List) GetLastN(n int) ([]string, error) {
return rl.LastN(n)
}
// Remove the first occurrence of an element from the list
func (rl *List) RemoveElement(value string) error {
conn := rl.pool.Get(rl.dbindex)
_, err := conn.Do("LREM", rl.id, value)
return err
}
// Set element of list at index n to value
func (rl *List) Set(index int64, value string) error {
conn := rl.pool.Get(rl.dbindex)
_, err := conn.Do("LSET", rl.id, index, value)
return err
}
// Trim an existing list so that it will contain only the specified range of
// elements specified.
func (rl *List) Trim(start, stop int64) error {
conn := rl.pool.Get(rl.dbindex)
_, err := conn.Do("LTRIM", rl.id, start, stop)
return err
}
// Remove this list
func (rl *List) Remove() error {
conn := rl.pool.Get(rl.dbindex)
_, err := conn.Do("DEL", rl.id)
return err
}
// Clear the contents
func (rl *List) Clear() error {
return rl.Remove()
}
/* --- Set functions --- */
// Create a new set
func NewSet(pool *ConnectionPool, id string) *Set {
return &Set{pool, id, 0}
}
// Select a different database
func (rs *Set) SelectDatabase(dbindex int) {
rs.dbindex = dbindex
}
// Add an element to the set
func (rs *Set) Add(value string) error {
conn := rs.pool.Get(rs.dbindex)
_, err := conn.Do("SADD", rs.id, value)
return err
}
// Returns the set cardinality (number of elements) of the set
func (rs *Set) Size() (int64, error) {
conn := rs.pool.Get(rs.dbindex)
size, err := conn.Do("SCARD", rs.id)
if err != nil {
panic(err)
}
return redis.Int64(size, err)
}
// Check if a given value is in the set
func (rs *Set) Has(value string) (bool, error) {
conn := rs.pool.Get(rs.dbindex)
retval, err := conn.Do("SISMEMBER", rs.id, value)
if err != nil {
panic(err)
}
return redis.Bool(retval, err)
}
// Get all elements of the set
func (rs *Set) All() ([]string, error) {
conn := rs.pool.Get(rs.dbindex)
result, err := redis.Values(conn.Do("SMEMBERS", rs.id))
strs := make([]string, len(result))
for i := 0; i < len(result); i++ {
strs[i] = getString(result, i)
}
return strs, err
}
// Deprecated
func (rs *Set) GetAll() ([]string, error) {
return rs.All()
}
// Remove a random member from the set
func (rs *Set) Pop() (string, error) {
conn := rs.pool.Get(rs.dbindex)
result, err := conn.Do("SPOP", rs.id)
if err != nil {
panic(err)
}
return redis.String(result, err)
}
// Get a random member of the set
func (rs *Set) Random() (string, error) {
conn := rs.pool.Get(rs.dbindex)
result, err := conn.Do("SRANDMEMBER", rs.id)
if err != nil {
panic(err)
}
return redis.String(result, err)
}
// Remove an element from the set
func (rs *Set) Del(value string) error {
conn := rs.pool.Get(rs.dbindex)
_, err := conn.Do("SREM", rs.id, value)
return err
}
// Remove this set
func (rs *Set) Remove() error {
conn := rs.pool.Get(rs.dbindex)
_, err := conn.Do("DEL", rs.id)
return err
}
// Clear the contents
func (rs *Set) Clear() error {
return rs.Remove()
}
/* --- HashMap functions --- */
// Create a new hashmap
func NewHashMap(pool *ConnectionPool, id string) *HashMap {
return &HashMap{pool, id, 0}
}
// Select a different database
func (rh *HashMap) SelectDatabase(dbindex int) {
rh.dbindex = dbindex
}
// Set a value in a hashmap given the element id (for instance a user id) and the key (for instance "password")
func (rh *HashMap) Set(elementid, key, value string) error {
conn := rh.pool.Get(rh.dbindex)
_, err := conn.Do("HSET", rh.id+":"+elementid, key, value)
return err
}
// Given an element id, set a key and a value together with an expiration time
func (rh *HashMap) SetExpire(elementid, key, value string, expire time.Duration) error {
conn := rh.pool.Get(rh.dbindex)
if _, err := conn.Do("HSET", rh.id+":"+elementid, key, value); err != nil {
return err
}
// No EXPIRE in Redis for hash keys, as far as I can tell from the documentation.
// This is the manual way.
go func() {
time.Sleep(expire)
rh.DelKey(elementid, key)
}()
// Set the elementid to expire in the given duration (as milliseconds)
//expireMilliseconds := expire.Nanoseconds() / 1000000
//if _, err := conn.Do("PEXPIRE", rh.id+":"+elementid, expireMilliseconds); err != nil {
// return err
//}
return nil
}
// Commented out because this would only return TTL for the elementid, not for the key
// TimeToLive returns how long a key has to live until it expires
// Returns a duration of 0 when the time has passed
//func (rh *HashMap) TimeToLive(elementid string) (time.Duration, error) {
// conn := rh.pool.Get(rh.dbindex)
// ttlSecondsInterface, err := conn.Do("TTL", rh.id+":"+elementid)
// if err != nil || ttlSecondsInterface.(int64) <= 0 {
// return time.Duration(0), err
// }
// ns := time.Duration(ttlSecondsInterface.(int64)) * time.Second
// return ns, nil
//}
// Get a value from a hashmap given the element id (for instance a user id) and the key (for instance "password")
func (rh *HashMap) Get(elementid, key string) (string, error) {
conn := rh.pool.Get(rh.dbindex)
result, err := redis.String(conn.Do("HGET", rh.id+":"+elementid, key))
if err != nil {
return "", err
}
return result, nil
}
// Check if a given elementid + key is in the hash map
func (rh *HashMap) Has(elementid, key string) (bool, error) {
conn := rh.pool.Get(rh.dbindex)
retval, err := conn.Do("HEXISTS", rh.id+":"+elementid, key)
if err != nil {
panic(err)
}
return redis.Bool(retval, err)
}
// Keys returns the keys of the given elementid.
func (rh *HashMap) Keys(elementid string) ([]string, error) {
conn := rh.pool.Get(rh.dbindex)
result, err := redis.Values(conn.Do("HKEYS", rh.id+":"+elementid))
strs := make([]string, len(result))
for i := 0; i < len(result); i++ {
strs[i] = getString(result, i)
}
return strs, err
}
// Check if a given elementid exists as a hash map at all
func (rh *HashMap) Exists(elementid string) (bool, error) {
// TODO: key is not meant to be a wildcard, check for "*"
return hasKey(rh.pool, rh.id+":"+elementid, rh.dbindex)
}
// Get all elementid's for all hash elements
func (rh *HashMap) All() ([]string, error) {
conn := rh.pool.Get(rh.dbindex)
result, err := redis.Values(conn.Do("KEYS", rh.id+":*"))
strs := make([]string, len(result))
idlen := len(rh.id)
for i := 0; i < len(result); i++ {
strs[i] = getString(result, i)[idlen+1:]
}
return strs, err
}
// Deprecated
func (rh *HashMap) GetAll() ([]string, error) {
return rh.All()
}
// Remove a key for an entry in a hashmap (for instance the email field for a user)
func (rh *HashMap) DelKey(elementid, key string) error {
conn := rh.pool.Get(rh.dbindex)
_, err := conn.Do("HDEL", rh.id+":"+elementid, key)
return err
}
// Remove an element (for instance a user)
func (rh *HashMap) Del(elementid string) error {
conn := rh.pool.Get(rh.dbindex)
_, err := conn.Do("DEL", rh.id+":"+elementid)
return err
}
// Remove this hashmap (all keys that starts with this hashmap id and a colon)
func (rh *HashMap) Remove() error {
conn := rh.pool.Get(rh.dbindex)
// Find all hashmap keys that starts with rh.id+":"
results, err := redis.Values(conn.Do("KEYS", rh.id+":*"))
if err != nil {
return err
}
// For each key id
for i := 0; i < len(results); i++ {
// Delete this key
if _, err = conn.Do("DEL", getString(results, i)); err != nil {
return err
}
}
return nil
}
// Clear the contents
func (rh *HashMap) Clear() error {
return rh.Remove()
}
/* --- KeyValue functions --- */
// Create a new key/value
func NewKeyValue(pool *ConnectionPool, id string) *KeyValue {
return &KeyValue{pool, id, 0}
}
// Select a different database
func (rkv *KeyValue) SelectDatabase(dbindex int) {
rkv.dbindex = dbindex
}
// Set a key and value
func (rkv *KeyValue) Set(key, value string) error {
conn := rkv.pool.Get(rkv.dbindex)
_, err := conn.Do("SET", rkv.id+":"+key, value)
return err
}
// Set a key and value, with expiry
func (rkv *KeyValue) SetExpire(key, value string, expire time.Duration) error {
conn := rkv.pool.Get(rkv.dbindex)
// Convert from nanoseconds to milliseconds
expireMilliseconds := expire.Nanoseconds() / 1000000
// Set the value, together with an expiry time, given in milliseconds
_, err := conn.Do("SET", rkv.id+":"+key, value, "PX", expireMilliseconds)
return err
}
// TimeToLive returns how long a key has to live until it expires
// Returns a duration of 0 when the time has passed
func (rkv *KeyValue) TimeToLive(key string) (time.Duration, error) {
conn := rkv.pool.Get(rkv.dbindex)
ttlSecondsInterface, err := conn.Do("TTL", rkv.id+":"+key)
if err != nil || ttlSecondsInterface.(int64) <= 0 {
return time.Duration(0), err
}
ns := time.Duration(ttlSecondsInterface.(int64)) * time.Second
return ns, nil
}
// Get a value given a key
func (rkv *KeyValue) Get(key string) (string, error) {
conn := rkv.pool.Get(rkv.dbindex)
result, err := redis.String(conn.Do("GET", rkv.id+":"+key))
if err != nil {
return "", err
}
return result, nil
}
// Remove a key
func (rkv *KeyValue) Del(key string) error {
conn := rkv.pool.Get(rkv.dbindex)
_, err := conn.Do("DEL", rkv.id+":"+key)
return err
}
// Increase the value of a key, returns the new value
// Returns an empty string if there were errors,
// or "0" if the key does not already exist.
func (rkv *KeyValue) Inc(key string) (string, error) {
conn := rkv.pool.Get(rkv.dbindex)
result, err := redis.Int64(conn.Do("INCR", rkv.id+":"+key))
if err != nil {
return "0", err
}
return strconv.FormatInt(result, 10), nil
}
// Remove this key/value
func (rkv *KeyValue) Remove() error {
conn := rkv.pool.Get(rkv.dbindex)
// Find all keys that starts with rkv.id+":"
results, err := redis.Values(conn.Do("KEYS", rkv.id+":*"))
if err != nil {
return err
}
// For each key id
for i := 0; i < len(results); i++ {
// Delete this key
if _, err = conn.Do("DEL", getString(results, i)); err != nil {
return err
}
}
return nil
}
// Clear the contents
func (rkv *KeyValue) Clear() error {
return rkv.Remove()
}
// --- Generic redis functions ---
// Check if a key exists. The key can be a wildcard (ie. "user*").
func hasKey(pool *ConnectionPool, wildcard string, dbindex int) (bool, error) {
conn := pool.Get(dbindex)
result, err := redis.Values(conn.Do("KEYS", wildcard))
if err != nil {
return false, err
}
return len(result) > 0, nil
}
// --- Related to setting and retrieving timeout values
// SetConnectTimeout sets the connect timeout for new connections
func SetConnectTimeout(t time.Duration) {
connectTimeout = t
}
// SetReadTimeout sets the read timeout for new connections
func SetReadTimeout(t time.Duration) {
readTimeout = t
}
// SetWriteTimeout sets the write timeout for new connections
func SetWriteTimeout(t time.Duration) {
writeTimeout = t
}
// SetIdleTimeout sets the idle timeout for new connections
func SetIdleTimeout(t time.Duration) {
idleTimeout = t
}
// ConnectTimeout returns the current connect timeout for new connections
func ConnectTimeout() time.Duration {
return connectTimeout
}
// ReadTimeout returns the current read timeout for new connections
func ReadTimeout() time.Duration {
return readTimeout
}
// WriteTimeout returns the current write timeout for new connections
func WriteTimeout() time.Duration {
return writeTimeout
}
// IdleTimeout returns the current idle timeout for new connections
func IdleTimeout() time.Duration {
return idleTimeout
}