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  }