github.com/klaytn/klaytn@v1.12.1/storage/statedb/cache_redis.go (about) 1 // Copyright 2020 The klaytn Authors 2 // This file is part of the klaytn library. 3 // 4 // The klaytn library is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU Lesser General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // The klaytn library is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU Lesser General Public License for more details. 13 // 14 // You should have received a copy of the GNU Lesser General Public License 15 // along with the klaytn library. If not, see <http://www.gnu.org/licenses/>. 16 17 package statedb 18 19 import ( 20 "errors" 21 "runtime" 22 "time" 23 24 "github.com/go-redis/redis/v7" 25 "github.com/klaytn/klaytn/common/hexutil" 26 ) 27 28 const ( 29 // Channel size for aync item set. If average item size is 400Byte, 4MB could be used. 30 redisSetItemChannelSize = 10000 31 // Channel size for block subscription. If average block size is 10KB, 10MB could be used. 32 redisSubscriptionChannelSize = 1000 33 redisSubscriptionChannelBlock = "latestBlock" 34 ) 35 36 var ( 37 redisCacheDialTimeout = time.Duration(900 * time.Millisecond) 38 redisCacheTimeout = time.Duration(900 * time.Millisecond) 39 40 errRedisNoEndpoint = errors.New("redis endpoint not specified") 41 ) 42 43 type RedisCache struct { 44 client redis.UniversalClient 45 setItemCh chan setItem 46 pubSub *redis.PubSub 47 } 48 49 type setItem struct { 50 key []byte 51 value []byte 52 } 53 54 func newRedisClient(endpoints []string, isCluster bool) (redis.UniversalClient, error) { 55 if endpoints == nil { 56 return nil, errRedisNoEndpoint 57 } 58 59 // cluster-enabled redis can have more than one shard 60 if isCluster { 61 return redis.NewClusterClient(&redis.ClusterOptions{ 62 // it takes Timeout * (MaxRetries+1) to raise an error 63 Addrs: endpoints, 64 DialTimeout: redisCacheDialTimeout, 65 ReadTimeout: redisCacheTimeout, 66 WriteTimeout: redisCacheTimeout, 67 MaxRetries: 2, 68 }), nil 69 } 70 71 return redis.NewClient(&redis.Options{ 72 // it takes Timeout * (MaxRetries+1) to raise an error 73 Addr: endpoints[0], 74 DialTimeout: redisCacheDialTimeout, 75 ReadTimeout: redisCacheTimeout, 76 WriteTimeout: redisCacheTimeout, 77 MaxRetries: 2, 78 }), nil 79 } 80 81 // newRedisCache creates a redis cache containing redis client, setItemCh and pubSub. 82 // It generates worker goroutines to process Set commands asynchronously. 83 func newRedisCache(config *TrieNodeCacheConfig) (*RedisCache, error) { 84 cli, err := newRedisClient(config.RedisEndpoints, config.RedisClusterEnable) 85 if err != nil { 86 logger.Error("failed to create a redis client", "err", err, "endpoint", config.RedisEndpoints, 87 "isCluster", config.RedisClusterEnable) 88 return nil, err 89 } 90 91 cache := &RedisCache{ 92 client: cli, 93 setItemCh: make(chan setItem, redisSetItemChannelSize), 94 pubSub: cli.Subscribe(), 95 } 96 97 workerNum := runtime.NumCPU()/2 + 1 98 for i := 0; i < workerNum; i++ { 99 go func() { 100 for item := range cache.setItemCh { 101 cache.Set(item.key, item.value) 102 } 103 }() 104 } 105 106 logger.Info("Initialized trie node cache with redis", "endpoint", config.RedisEndpoints, 107 "isCluster", config.RedisClusterEnable) 108 return cache, nil 109 } 110 111 func (cache *RedisCache) Get(k []byte) []byte { 112 val, err := cache.client.Get(hexutil.Encode(k)).Bytes() 113 if err != nil { 114 logger.Debug("cannot get an item from redis cache", "err", err, "key", hexutil.Encode(k)) 115 return nil 116 } 117 return val 118 } 119 120 // Set writes data synchronously. 121 // To write data asynchronously, use SetAsync instead. 122 func (cache *RedisCache) Set(k, v []byte) { 123 if err := cache.client.Set(hexutil.Encode(k), v, 0).Err(); err != nil { 124 logger.Error("failed to set an item on redis cache", "err", err, "key", hexutil.Encode(k)) 125 } 126 } 127 128 // SetAsync writes data asynchronously. Not all data is written if a setItemCh is full. 129 // To write data synchronously, use Set instead. 130 func (cache *RedisCache) SetAsync(k, v []byte) { 131 item := setItem{key: k, value: v} 132 select { 133 case cache.setItemCh <- item: 134 default: 135 logger.Warn("redis setItem channel is full") 136 } 137 } 138 139 func (cache *RedisCache) Has(k []byte) ([]byte, bool) { 140 val := cache.Get(k) 141 if val == nil { 142 return nil, false 143 } 144 return val, true 145 } 146 147 func (cache *RedisCache) publish(channel string, msg string) error { 148 return cache.client.Publish(channel, msg).Err() 149 } 150 151 // subscribe subscribes the redis client to the given channel. 152 // It returns an existing *redis.PubSub subscribing previously registered channels also. 153 func (cache *RedisCache) subscribe(channel string) *redis.PubSub { 154 if err := cache.pubSub.Subscribe(channel); err != nil { 155 logger.Error("failed to subscribe channel", "err", err, "channel", channel) 156 } 157 return cache.pubSub 158 } 159 160 func (cache *RedisCache) PublishBlock(msg string) error { 161 return cache.publish(redisSubscriptionChannelBlock, msg) 162 } 163 164 func (cache *RedisCache) SubscribeBlockCh() <-chan *redis.Message { 165 return cache.subscribe(redisSubscriptionChannelBlock).ChannelSize(redisSubscriptionChannelSize) 166 } 167 168 func (cache *RedisCache) UnsubscribeBlock() error { 169 return cache.pubSub.Unsubscribe(redisSubscriptionChannelBlock) 170 } 171 172 func (cache *RedisCache) UpdateStats() interface{} { 173 return nil 174 } 175 176 func (cache *RedisCache) SaveToFile(filePath string, concurrency int) error { 177 return nil 178 } 179 180 func (cache *RedisCache) Close() error { 181 cache.pubSub.Close() 182 close(cache.setItemCh) 183 return cache.client.Close() 184 }