github.com/cloudwego/hertz@v0.9.3/pkg/app/client/loadbalance/lbcache.go (about)

     1  /*
     2   * Copyright 2022 CloudWeGo Authors
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *     http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  package loadbalance
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"sync"
    23  	"sync/atomic"
    24  	"time"
    25  
    26  	"github.com/cloudwego/hertz/pkg/app/client/discovery"
    27  	"github.com/cloudwego/hertz/pkg/common/errors"
    28  	"github.com/cloudwego/hertz/pkg/common/hlog"
    29  	"github.com/cloudwego/hertz/pkg/protocol"
    30  	"golang.org/x/sync/singleflight"
    31  )
    32  
    33  type cacheResult struct {
    34  	res         atomic.Value // newest and previous discovery result
    35  	expire      int32        // 0 = normal, 1 = expire and collect next ticker
    36  	serviceName string       // service psm
    37  }
    38  
    39  var (
    40  	balancerFactories    sync.Map // key: resolver name + load-balancer name
    41  	balancerFactoriesSfg singleflight.Group
    42  )
    43  
    44  func cacheKey(resolver, balancer string, opts Options) string {
    45  	return fmt.Sprintf("%s|%s|{%s %s}", resolver, balancer, opts.RefreshInterval, opts.ExpireInterval)
    46  }
    47  
    48  type BalancerFactory struct {
    49  	opts     Options
    50  	cache    sync.Map // key -> LoadBalancer
    51  	resolver discovery.Resolver
    52  	balancer Loadbalancer
    53  	sfg      singleflight.Group
    54  }
    55  
    56  type Config struct {
    57  	Resolver discovery.Resolver
    58  	Balancer Loadbalancer
    59  	LbOpts   Options
    60  }
    61  
    62  // NewBalancerFactory get or create a balancer with given target.
    63  // If it has the same key(resolver.Target(target)), we will cache and reuse the Balance.
    64  func NewBalancerFactory(config Config) *BalancerFactory {
    65  	config.LbOpts.Check()
    66  	uniqueKey := cacheKey(config.Resolver.Name(), config.Balancer.Name(), config.LbOpts)
    67  	val, ok := balancerFactories.Load(uniqueKey)
    68  	if ok {
    69  		return val.(*BalancerFactory)
    70  	}
    71  	val, _, _ = balancerFactoriesSfg.Do(uniqueKey, func() (interface{}, error) {
    72  		b := &BalancerFactory{
    73  			opts:     config.LbOpts,
    74  			resolver: config.Resolver,
    75  			balancer: config.Balancer,
    76  		}
    77  		go b.watcher()
    78  		go b.refresh()
    79  		balancerFactories.Store(uniqueKey, b)
    80  		return b, nil
    81  	})
    82  	return val.(*BalancerFactory)
    83  }
    84  
    85  // watch expired balancer
    86  func (b *BalancerFactory) watcher() {
    87  	for range time.Tick(b.opts.ExpireInterval) {
    88  		b.cache.Range(func(key, value interface{}) bool {
    89  			cache := value.(*cacheResult)
    90  			if atomic.CompareAndSwapInt32(&cache.expire, 0, 1) {
    91  				// 1. set expire flag
    92  				// 2. wait next ticker for collect, maybe the balancer is used again
    93  				// (avoid being immediate delete the balancer which had been created recently)
    94  			} else {
    95  				b.cache.Delete(key)
    96  				b.balancer.Delete(key.(string))
    97  			}
    98  			return true
    99  		})
   100  	}
   101  }
   102  
   103  // cache key with resolver name prefix avoid conflict for balancer
   104  func renameResultCacheKey(res *discovery.Result, resolverName string) {
   105  	res.CacheKey = resolverName + ":" + res.CacheKey
   106  }
   107  
   108  // refresh is used to update service discovery information periodically.
   109  func (b *BalancerFactory) refresh() {
   110  	for range time.Tick(b.opts.RefreshInterval) {
   111  		b.cache.Range(func(key, value interface{}) bool {
   112  			res, err := b.resolver.Resolve(context.Background(), key.(string))
   113  			if err != nil {
   114  				hlog.SystemLogger().Warnf("resolver refresh failed, key=%s error=%s", key, err.Error())
   115  				return true
   116  			}
   117  			renameResultCacheKey(&res, b.resolver.Name())
   118  			cache := value.(*cacheResult)
   119  			cache.res.Store(res)
   120  			atomic.StoreInt32(&cache.expire, 0)
   121  			b.balancer.Rebalance(res)
   122  			return true
   123  		})
   124  	}
   125  }
   126  
   127  func (b *BalancerFactory) GetInstance(ctx context.Context, req *protocol.Request) (discovery.Instance, error) {
   128  	cacheRes, err := b.getCacheResult(ctx, req)
   129  	if err != nil {
   130  		return nil, err
   131  	}
   132  	atomic.StoreInt32(&cacheRes.expire, 0)
   133  	ins := b.balancer.Pick(cacheRes.res.Load().(discovery.Result))
   134  	if ins == nil {
   135  		hlog.SystemLogger().Errorf("null instance. serviceName: %s, options: %v", string(req.Host()), req.Options())
   136  		return nil, errors.NewPublic("instance not found")
   137  	}
   138  	return ins, nil
   139  }
   140  
   141  func (b *BalancerFactory) getCacheResult(ctx context.Context, req *protocol.Request) (*cacheResult, error) {
   142  	target := b.resolver.Target(ctx, &discovery.TargetInfo{Host: string(req.Host()), Tags: req.Options().Tags()})
   143  	cr, existed := b.cache.Load(target)
   144  	if existed {
   145  		return cr.(*cacheResult), nil
   146  	}
   147  	cr, err, _ := b.sfg.Do(target, func() (interface{}, error) {
   148  		cache := &cacheResult{
   149  			serviceName: string(req.Host()),
   150  		}
   151  		res, err := b.resolver.Resolve(ctx, target)
   152  		if err != nil {
   153  			return cache, err
   154  		}
   155  		renameResultCacheKey(&res, b.resolver.Name())
   156  		cache.res.Store(res)
   157  		atomic.StoreInt32(&cache.expire, 0)
   158  		b.balancer.Rebalance(res)
   159  		b.cache.Store(target, cache)
   160  		return cache, nil
   161  	})
   162  	if err != nil {
   163  		return nil, err
   164  	}
   165  	return cr.(*cacheResult), nil
   166  }