github.com/projecteru2/core@v0.0.0-20240321043226-06bcc1c23f58/engine/factory/factory.go (about)

     1  package factory
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strings"
     7  	"sync"
     8  	"time"
     9  
    10  	"github.com/alphadose/haxmap"
    11  	"github.com/cockroachdb/errors"
    12  	"github.com/panjf2000/ants/v2"
    13  
    14  	"github.com/projecteru2/core/engine"
    15  	"github.com/projecteru2/core/engine/docker"
    16  	"github.com/projecteru2/core/engine/fake"
    17  	"github.com/projecteru2/core/engine/mocks/fakeengine"
    18  	"github.com/projecteru2/core/engine/systemd"
    19  	"github.com/projecteru2/core/engine/virt"
    20  	"github.com/projecteru2/core/log"
    21  	"github.com/projecteru2/core/store"
    22  	"github.com/projecteru2/core/types"
    23  	"github.com/projecteru2/core/utils"
    24  )
    25  
    26  type factory func(ctx context.Context, config types.Config, nodename, endpoint, ca, cert, key string) (engine.API, error)
    27  
    28  var (
    29  	engines = map[string]factory{
    30  		docker.TCPPrefixKey:  docker.MakeClient,
    31  		docker.SockPrefixKey: docker.MakeClient,
    32  		virt.GRPCPrefixKey:   virt.MakeClient,
    33  		systemd.TCPPrefix:    systemd.MakeClient,
    34  		fakeengine.PrefixKey: fakeengine.MakeClient,
    35  	}
    36  	engineCache *EngineCache
    37  )
    38  
    39  // EngineCache .
    40  type EngineCache struct {
    41  	cache       *utils.EngineCache
    42  	keysToCheck *haxmap.Map[string, engineParams]
    43  	pool        *ants.PoolWithFunc
    44  	config      types.Config
    45  	stor        store.Store
    46  }
    47  
    48  // NewEngineCache .
    49  func NewEngineCache(config types.Config, stor store.Store) *EngineCache {
    50  	pool, _ := utils.NewPool(config.MaxConcurrency)
    51  	return &EngineCache{
    52  		cache:       utils.NewEngineCache(12*time.Hour, 10*time.Minute),
    53  		keysToCheck: haxmap.New[string, engineParams](),
    54  		pool:        pool,
    55  		config:      config,
    56  		stor:        stor,
    57  	}
    58  }
    59  
    60  // InitEngineCache init engine cache and start engine cache checker
    61  func InitEngineCache(ctx context.Context, config types.Config, stor store.Store) {
    62  	engineCache = NewEngineCache(config, stor)
    63  	// init the cache, we don't care the return values
    64  	if stor != nil {
    65  		_, _ = engineCache.stor.GetNodesByPod(ctx, &types.NodeFilter{
    66  			All: true,
    67  		})
    68  	}
    69  	go engineCache.CheckAlive(ctx)
    70  	go engineCache.CheckNodeStatus(ctx)
    71  }
    72  
    73  // Get .
    74  func (e *EngineCache) Get(key string) engine.API {
    75  	return e.cache.Get(key)
    76  }
    77  
    78  // Set .
    79  func (e *EngineCache) Set(params engineParams, client engine.API) {
    80  	e.cache.Set(params.getCacheKey(), client)
    81  	e.keysToCheck.Set(params.getCacheKey(), params)
    82  }
    83  
    84  // Delete .
    85  func (e *EngineCache) Delete(key string) {
    86  	e.cache.Delete(key)
    87  }
    88  
    89  // CheckAlive checks if the engine in cache is available
    90  func (e *EngineCache) CheckAlive(ctx context.Context) {
    91  	logger := log.WithFunc("engine.factory.CheckAlive")
    92  	logger.Info(ctx, "check alive starts")
    93  	defer logger.Info(ctx, "check alive ends")
    94  	defer e.pool.Release()
    95  	for {
    96  		select {
    97  		case <-ctx.Done():
    98  			return
    99  		default:
   100  		}
   101  
   102  		paramsChan := make(chan engineParams)
   103  		go func() {
   104  			e.keysToCheck.ForEach(func(_ string, v engineParams) bool {
   105  				paramsChan <- v
   106  				return true
   107  			})
   108  			close(paramsChan)
   109  		}()
   110  
   111  		wg := &sync.WaitGroup{}
   112  		for params := range paramsChan {
   113  			wg.Add(1)
   114  			params := params
   115  			_ = e.pool.Invoke(func() {
   116  				defer wg.Done()
   117  				cacheKey := params.getCacheKey()
   118  				client := e.cache.Get(cacheKey)
   119  				if client == nil {
   120  					e.cache.Delete(params.getCacheKey())
   121  					e.keysToCheck.Del(cacheKey)
   122  					return
   123  				}
   124  				if _, ok := client.(*fake.EngineWithErr); ok {
   125  					if newClient, err := newEngine(ctx, e.config, params.nodename, params.endpoint, params.ca, params.key, params.cert); err != nil {
   126  						logger.Errorf(ctx, err, "engine %+v is still unavailable", cacheKey)
   127  						e.cache.Set(cacheKey, &fake.EngineWithErr{DefaultErr: err})
   128  						// check node status
   129  						e.checkOneNodeStatus(ctx, &params)
   130  					} else {
   131  						e.cache.Set(cacheKey, newClient)
   132  					}
   133  					return
   134  				}
   135  				if err := validateEngine(ctx, client, e.config.ConnectionTimeout); err != nil {
   136  					logger.Errorf(ctx, err, "engine %+v is unavailable, will be replaced and removed", cacheKey)
   137  					e.cache.Set(cacheKey, &fake.EngineWithErr{DefaultErr: err})
   138  				}
   139  				logger.Debugf(ctx, "engine %+v is available", cacheKey)
   140  			})
   141  		}
   142  		wg.Wait()
   143  		time.Sleep(e.config.ConnectionTimeout)
   144  	}
   145  }
   146  
   147  func (e *EngineCache) CheckNodeStatus(ctx context.Context) {
   148  	logger := log.WithFunc("engine.factory.CheckNodeStatus")
   149  	logger.Info(ctx, "check NodeStatus starts")
   150  	defer logger.Info(ctx, "check NodeStatus ends")
   151  	if e.stor == nil {
   152  		logger.Warnf(ctx, "nodeStore is nil")
   153  		return
   154  	}
   155  	for {
   156  		select {
   157  		case <-ctx.Done():
   158  			return
   159  		default:
   160  		}
   161  		ch := e.stor.NodeStatusStream(ctx)
   162  
   163  		for ns := range ch {
   164  			if ns.Alive {
   165  				// GetNode will call GetEngine, so GetNode updates the engine cache automatically
   166  				if _, err := e.stor.GetNode(ctx, ns.Nodename); err != nil {
   167  					logger.Warnf(ctx, "failed to get node %s: %s", ns.Nodename, err)
   168  				}
   169  				continue
   170  			}
   171  			// a node may have multiple engines, so we need check all key here
   172  			e.keysToCheck.ForEach(func(_ string, ep engineParams) bool {
   173  				if ep.nodename == ns.Nodename {
   174  					logger.Infof(ctx, "remove engine %+v from cache", ep.getCacheKey())
   175  					RemoveEngineFromCache(ctx, ep.endpoint, ep.ca, ep.cert, ep.key)
   176  				}
   177  				return true
   178  			})
   179  		}
   180  	}
   181  }
   182  
   183  // GetEngineFromCache .
   184  func GetEngineFromCache(_ context.Context, endpoint, ca, cert, key string) engine.API {
   185  	return engineCache.Get(getEngineCacheKey(endpoint, ca, cert, key))
   186  }
   187  
   188  // RemoveEngineFromCache .
   189  func RemoveEngineFromCache(ctx context.Context, endpoint, ca, cert, key string) {
   190  	cacheKey := getEngineCacheKey(endpoint, ca, cert, key)
   191  	log.WithFunc("engine.factory.RemoveEngineFromCache").Infof(ctx, "remove engine %+v from cache", cacheKey)
   192  	engineCache.Delete(cacheKey)
   193  }
   194  
   195  // GetEngine get engine with cache
   196  func GetEngine(ctx context.Context, config types.Config, nodename, endpoint, ca, cert, key string) (client engine.API, err error) {
   197  	logger := log.WithFunc("engine.factory.GetEngine")
   198  	if client = GetEngineFromCache(ctx, endpoint, ca, cert, key); client != nil {
   199  		return client, nil
   200  	}
   201  
   202  	defer func() {
   203  		params := engineParams{
   204  			nodename: nodename,
   205  			endpoint: endpoint,
   206  			ca:       ca,
   207  			cert:     cert,
   208  			key:      key,
   209  		}
   210  		cacheKey := params.getCacheKey()
   211  		if err == nil {
   212  			engineCache.Set(params, client)
   213  			logger.Infof(ctx, "store engine %+v in cache", cacheKey)
   214  		} else {
   215  			engineCache.Set(params, &fake.EngineWithErr{DefaultErr: err})
   216  			logger.Infof(ctx, "store fake engine %+v in cache", cacheKey)
   217  		}
   218  	}()
   219  
   220  	return newEngine(ctx, config, nodename, endpoint, ca, cert, key)
   221  }
   222  
   223  type engineParams struct {
   224  	nodename string
   225  	endpoint string
   226  	ca       string
   227  	cert     string
   228  	key      string
   229  }
   230  
   231  func (ep engineParams) getCacheKey() string {
   232  	return getEngineCacheKey(ep.endpoint, ep.ca, ep.cert, ep.key)
   233  }
   234  
   235  func validateEngine(ctx context.Context, engine engine.API, timeout time.Duration) (err error) {
   236  	utils.WithTimeout(ctx, timeout, func(ctx context.Context) {
   237  		err = engine.Ping(ctx)
   238  	})
   239  	return err
   240  }
   241  
   242  func getEnginePrefix(endpoint string) (string, error) {
   243  	for prefix := range engines {
   244  		if strings.HasPrefix(endpoint, prefix) {
   245  			return prefix, nil
   246  		}
   247  	}
   248  	return "", errors.Wrapf(types.ErrInvaildNodeEndpoint, "endpoint invalid %+v", endpoint)
   249  }
   250  
   251  func getEngineCacheKey(endpoint, ca, cert, key string) string {
   252  	return fmt.Sprintf("%+v-%+v", endpoint, utils.SHA256(fmt.Sprintf(":%+v:%+v:%+v", ca, cert, key))[:8])
   253  }
   254  
   255  // newEngine get engine
   256  func newEngine(ctx context.Context, config types.Config, nodename, endpoint, ca, cert, key string) (client engine.API, err error) {
   257  	prefix, err := getEnginePrefix(endpoint)
   258  	if err != nil {
   259  		return nil, err
   260  	}
   261  	e, ok := engines[prefix]
   262  	if !ok {
   263  		return nil, types.ErrInvaildEngineEndpoint
   264  	}
   265  	utils.WithTimeout(ctx, config.ConnectionTimeout, func(ctx context.Context) {
   266  		client, err = e(ctx, config, nodename, endpoint, ca, cert, key)
   267  	})
   268  	if err != nil {
   269  		return nil, err
   270  	}
   271  	if err = validateEngine(ctx, client, config.ConnectionTimeout); err != nil {
   272  		log.WithFunc("engine.factory.newEngine").Errorf(ctx, err, "engine of %+v is unavailable", endpoint)
   273  		return nil, err
   274  	}
   275  	return client, nil
   276  }
   277  
   278  func (e *EngineCache) checkOneNodeStatus(ctx context.Context, params *engineParams) {
   279  	if e.stor == nil {
   280  		return
   281  	}
   282  	logger := log.WithFunc("engine.factory.checkOneNodeStatus")
   283  	nodename := params.nodename
   284  	cacheKey := params.getCacheKey()
   285  	if ns, err := e.stor.GetNodeStatus(ctx, nodename); (err != nil && errors.Is(err, types.ErrInvaildCount)) || (!ns.Alive) {
   286  		logger.Warnf(ctx, "node %s is offline, the cache will be removed", nodename)
   287  		e.Delete(cacheKey)
   288  	}
   289  }