github.com/angenalZZZ/gofunc@v0.0.0-20210507121333-48ff1be3917b/rpc/nats/subscriber_fastcache.go (about)

     1  package nats
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"log"
     8  	"os"
     9  	"path/filepath"
    10  	"regexp"
    11  	"sort"
    12  	"strconv"
    13  	"strings"
    14  	"sync/atomic"
    15  	"syscall"
    16  	"time"
    17  
    18  	"github.com/angenalZZZ/gofunc/data/cache/fastcache"
    19  	"github.com/angenalZZZ/gofunc/f"
    20  	"github.com/nats-io/nats.go"
    21  )
    22  
    23  // SubscriberFastCache The NatS Subscriber with Fast Cache Temporary Storage.
    24  type SubscriberFastCache struct {
    25  	*nats.Conn
    26  	sub  *nats.Subscription
    27  	Subj string
    28  	Hand func([][]byte) error
    29  	*fastcache.Cache
    30  	CacheDir     string // sets cache persist to disk directory
    31  	Index        uint64
    32  	Count        uint64
    33  	Since        *f.TimeStamp
    34  	MsgLimit     int   // sets the limits for pending messages for this subscription.
    35  	BytesLimit   int   // sets the limits for a message's bytes for this subscription.
    36  	OnceAmount   int64 // sets amount allocated at one time
    37  	OnceInterval time.Duration
    38  	Running      bool
    39  	async        bool
    40  	err          error
    41  	pool         *f.Pool
    42  }
    43  
    44  // NewSubscriberFastCache Create a subscriber with cache store for Client Connect.
    45  func NewSubscriberFastCache(nc *nats.Conn, subject string, cacheDir ...string) *SubscriberFastCache {
    46  	dir, since := ".nats", f.TimeFrom(time.Now(), true)
    47  
    48  	if len(cacheDir) == 1 && cacheDir[0] != "" {
    49  		if err := f.Mkdir(cacheDir[0]); err != nil {
    50  			return nil
    51  		}
    52  		dir = filepath.Join(cacheDir[0], since.LocalTimeStampString(true))
    53  	} else {
    54  		if err := f.MkdirCurrent(dir); err != nil {
    55  			return nil
    56  		}
    57  		dir = filepath.Join(f.CurrentDir(), dir, since.LocalTimeStampString(true))
    58  	}
    59  
    60  	client := fastcache.New(1073741824) // 1GB cache capacity
    61  
    62  	sub := &SubscriberFastCache{
    63  		Conn:         nc,
    64  		Subj:         subject,
    65  		Cache:        client, // fast cache
    66  		CacheDir:     dir,
    67  		Since:        since,
    68  		MsgLimit:     100000000, // pending messages: 100 million
    69  		BytesLimit:   1048576,   // a message's size: 1MB
    70  		OnceAmount:   -1,
    71  		OnceInterval: time.Second,
    72  		async:        true,
    73  	}
    74  
    75  	sub.pool = f.NewPool(f.NumCPUx16, func() f.PoolWorker {
    76  		return &CachePoolWorker{
    77  			processor: sub.Process,
    78  		}
    79  	})
    80  
    81  	return sub
    82  }
    83  
    84  // LimitMessage sets amount for pending messages for this subscription, and a message's bytes.
    85  // Defaults amountPendingMessages: 100 million, anMessageBytes: 1MB
    86  func (sub *SubscriberFastCache) LimitMessage(amountPendingMessages, anMessageBytes int) {
    87  	sub.MsgLimit, sub.BytesLimit = amountPendingMessages, anMessageBytes
    88  }
    89  
    90  // LimitAmount sets amount allocated at one time, and the processing interval time.
    91  // Defaults onceAmount: -1, onceInterval: time.Second
    92  func (sub *SubscriberFastCache) LimitAmount(onceAmount int64, onceInterval time.Duration) {
    93  	sub.OnceAmount, sub.OnceInterval = onceAmount, onceInterval
    94  }
    95  
    96  // Process messages for this subscription.
    97  func (sub *SubscriberFastCache) Process(msg *CacheMsg) error {
    98  	key, val := msg.Key, msg.Val
    99  	sub.Cache.Set(f.BytesUint64(key), val)
   100  	return nil
   101  }
   102  
   103  // Run runtime to end your application.
   104  func (sub *SubscriberFastCache) Run(waitFunc ...func()) {
   105  	sub.Running = true
   106  	ctx, cancel := context.WithCancel(context.Background())
   107  
   108  	// Handle panic.
   109  	defer func() {
   110  		err := recover()
   111  		if err != nil {
   112  			Log.Error().Msgf("[nats] stop receive new data with error.panic > %v", err)
   113  		} else {
   114  			Log.Warn().Msgf("[nats] stop receive new data > %d/%d < %d records not processed", sub.Index, sub.Count, sub.Count-sub.Index)
   115  		}
   116  
   117  		// Unsubscribe will remove interest in the given subject.
   118  		_ = sub.sub.Unsubscribe()
   119  		// Drain connection (Preferred for responders), Close() not needed if this is called.
   120  		_ = sub.Conn.Drain()
   121  
   122  		// Stop handle new data.
   123  		cancel()
   124  		// Stop pool processor.
   125  		sub.pool.Close()
   126  		// Save not processed data.
   127  		sub.Save(sub.CacheDir)
   128  
   129  		// os.Exit(1)
   130  		if err != nil {
   131  			log.Fatal(err)
   132  		}
   133  		sub.Running = false
   134  	}()
   135  
   136  	// Async Subscriber.
   137  	sub.sub, sub.err = sub.Conn.Subscribe(sub.Subj, func(msg *nats.Msg) {
   138  		key, val := atomic.AddUint64(&sub.Count, 1), msg.Data
   139  		//sub.pool.Process(&CacheMsg{Key: key, Val: val}) // It's slow
   140  		sub.Cache.Set(f.BytesUint64(key), val)
   141  	})
   142  	// Set listening.
   143  	SubscribeErrorHandle(sub.sub, sub.async, sub.err)
   144  	if sub.err != nil {
   145  		log.Fatal(sub.err)
   146  	}
   147  
   148  	// Set pending limits.
   149  	SubscribeLimitHandle(sub.sub, sub.MsgLimit, sub.BytesLimit)
   150  
   151  	// Flush connection to server, returns when all messages have been processed.
   152  	FlushAndCheckLastError(sub.Conn)
   153  
   154  	// init handle old data.
   155  	go sub.init(ctx)
   156  
   157  	// run handle new data.
   158  	go sub.hand(ctx)
   159  
   160  	if len(waitFunc) > 0 {
   161  		Log.Info().Msg("[nats] start running and wait to auto exit")
   162  		waitFunc[0]()
   163  		return
   164  	}
   165  
   166  	Log.Info().Msg("[nats] start running and wait to manual exit")
   167  
   168  	// Pass the signals you want to end your application.
   169  	death := f.NewDeath(syscall.SIGINT, syscall.SIGTERM)
   170  	// When you want to block for shutdown signals.
   171  	death.WaitForDeathWithFunc(func() {
   172  		Log.Warn().Msg("[nats] forced to shutdown.")
   173  	})
   174  }
   175  
   176  // Stop runtime to end your application.
   177  func (sub *SubscriberFastCache) Stop(ms ...int) {
   178  	n := 10000
   179  	if len(ms) > 0 {
   180  		n = ms[0]
   181  	}
   182  	for ; sub.Running && n > 0; n-- {
   183  		time.Sleep(time.Millisecond)
   184  	}
   185  }
   186  
   187  // init handle old data.
   188  func (sub *SubscriberFastCache) init(ctx context.Context) {
   189  	if sub.Hand == nil {
   190  		return
   191  	}
   192  
   193  	cacheDir := sub.CacheDir
   194  	dir0, parentDir := filepath.Base(cacheDir), filepath.Dir(cacheDir)
   195  	oldDirs, err := filepath.Glob(filepath.Join(parentDir, "*"))
   196  	if err != nil || oldDirs == nil || len(oldDirs) == 0 {
   197  		return
   198  	}
   199  	sort.Strings(oldDirs)
   200  
   201  	var clearCache = func(cache *fastcache.Cache, index, count int64) {
   202  		for i, c := uint64(index)+1, uint64(count); i <= c; i++ {
   203  			k := f.BytesUint64(i)
   204  			cache.Del(k)
   205  		}
   206  	}
   207  
   208  	handRecords, oldRecords, onceRecords := 0, 0, atomic.LoadInt64(&sub.OnceAmount)
   209  	var runHandle = func(dir string, dirname string) (ok bool) {
   210  		oldFiles, err := filepath.Glob(filepath.Join(dir, "*"))
   211  		if err != nil || oldFiles == nil || len(oldFiles) == 0 {
   212  			ok = true
   213  			return
   214  		}
   215  		sort.Strings(oldFiles)
   216  		for _, oldFile := range oldFiles {
   217  			_, jsonFile := filepath.Split(oldFile)
   218  			if ok, _ := regexp.MatchString(`^\d+\.\d+\.\d+\.json$`, jsonFile); !ok {
   219  				continue
   220  			}
   221  
   222  			fileDir := strings.Replace(jsonFile, ".json", "", 1)
   223  			filePath := filepath.Join(dir, fileDir)
   224  			if f.PathExists(filePath) == false {
   225  				continue
   226  			}
   227  
   228  			cache, err := fastcache.LoadFromFile(filePath)
   229  			s := strings.Split(fileDir, ".")
   230  			if err != nil || len(s) != 3 {
   231  				continue
   232  			}
   233  
   234  			since := s[0]
   235  			index, _ := strconv.ParseInt(s[1], 10, 0)
   236  			count, _ := strconv.ParseInt(s[2], 10, 0)
   237  			indexZero, indexSize := uint64(index)+1, count-index
   238  			if onceRecords > 0 {
   239  				indexSize = onceRecords
   240  			}
   241  
   242  			var handData = make([][]byte, 0, indexSize)
   243  			for i, c, dataIndex := indexZero, uint64(count), int64(0); i <= c; i++ {
   244  				if key := f.BytesUint64(i); cache.Has(key) {
   245  					val := cache.Get(nil, key)
   246  					handData = append(handData, val)
   247  					if dataIndex++; dataIndex == onceRecords || i == c {
   248  						// bulk handle
   249  						if err := sub.Hand(handData); err != nil {
   250  							// rollback
   251  							Log.Error().Msgf("[nats] init handle old data > %s > %s", dirname, err)
   252  							if i > indexZero {
   253  								clearCache(cache, int64(indexZero)-1, int64(i))
   254  								dirname1, filename1 := sub.dirnames(since, i-1, c), sub.filenames(since, i-1, c)
   255  								saveFastCache(cache, dir, dirname1, filename1)
   256  								_ = os.Remove(oldFile)
   257  								_ = os.RemoveAll(filePath)
   258  							}
   259  							return
   260  						}
   261  						handRecords += len(handData)
   262  						// reset data
   263  						dataIndex = 0
   264  						handData = make([][]byte, 0, indexSize)
   265  						time.Sleep(sub.OnceInterval)
   266  					}
   267  				}
   268  			}
   269  
   270  			_ = os.Remove(oldFile)
   271  			_ = os.RemoveAll(filePath)
   272  		}
   273  
   274  		ok = true
   275  		return
   276  	}
   277  
   278  	for _, oldDir := range oldDirs {
   279  		if !f.IsDir(oldDir) {
   280  			continue
   281  		}
   282  
   283  		dir1 := filepath.Base(oldDir)
   284  		if ok, _ := regexp.MatchString(`^\d+$`, dir1); !ok {
   285  			continue
   286  		}
   287  		if len(dir1) != 14 || dir1 == dir0 {
   288  			continue
   289  		}
   290  		if datFiles, err := filepath.Glob(filepath.Join(oldDir, "*.json")); err != nil || datFiles == nil || len(datFiles) == 0 {
   291  			continue
   292  		}
   293  
   294  		// reboot init handle old data
   295  		for f.PathExists(oldDir) {
   296  			if runHandle(oldDir, dir1) {
   297  				if err1 := os.RemoveAll(oldDir); err1 != nil {
   298  					Log.Error().Msgf("[nats] remove old data directory > %s > %s", dir1, err1)
   299  				}
   300  			}
   301  			Log.Info().Msgf("[nats] init handle old data > %d records < %s", handRecords, dir1)
   302  			oldRecords += handRecords
   303  			handRecords = 0
   304  		}
   305  	}
   306  
   307  	if oldRecords > 0 {
   308  		Log.Info().Msgf("[nats] init handle old data > %d records", oldRecords)
   309  	}
   310  
   311  	if err := ctx.Err(); err != nil && err != context.Canceled {
   312  		Log.Warn().Msgf("[nats] init handle old data err > %s", err)
   313  	}
   314  }
   315  
   316  // run handle new data.
   317  func (sub *SubscriberFastCache) hand(ctx context.Context) {
   318  	if sub.Hand == nil {
   319  		return
   320  	}
   321  
   322  	var (
   323  		running  bool
   324  		runCount uint64
   325  		delIndex uint64
   326  	)
   327  
   328  	var runHandle = func() {
   329  		count, index := atomic.LoadUint64(&sub.Count), atomic.LoadUint64(&sub.Index)
   330  		if count <= runCount {
   331  			// reset handle
   332  			if 0 < count && 3 == time.Now().Hour() && index <= delIndex {
   333  				sub.Count, runCount, sub.Index, delIndex = 0, 0, 0, 0
   334  			}
   335  			return
   336  		}
   337  
   338  		handRecords, onceRecords := int64(0), atomic.LoadInt64(&sub.OnceAmount)
   339  		indexSize := int64(count - index)
   340  		if onceRecords > 0 {
   341  			indexSize = onceRecords
   342  		}
   343  
   344  		var handData = make([][]byte, 0, indexSize)
   345  		for dataIndex := int64(0); dataIndex < indexSize && index < count; runCount++ {
   346  			index++ // key equals index
   347  			val := sub.Cache.Get(nil, f.BytesUint64(index))
   348  			if val == nil {
   349  				val = []byte{}
   350  			}
   351  			handData = append(handData, val)
   352  			handRecords++
   353  			if dataIndex++; dataIndex == indexSize || index == count {
   354  				// bulk handle
   355  				if err := sub.Hand(handData); err != nil {
   356  					// rollback
   357  					Log.Error().Msgf("[nats] run handle new data err > %s", err)
   358  					handRecords -= dataIndex
   359  					break
   360  				}
   361  				sub.Index += uint64(dataIndex)
   362  				// reset data
   363  				dataIndex = 0
   364  				handData = make([][]byte, 0, indexSize)
   365  			}
   366  		}
   367  
   368  		if handRecords == 0 {
   369  			return
   370  		}
   371  
   372  		Log.Info().Msgf("[nats] run handle new data > %d/%d < %d records", sub.Index, sub.Count, handRecords)
   373  
   374  		// delete old data
   375  		go func(index, handRecords uint64) {
   376  			for i, n := index-handRecords+1, index; i <= n; i++ {
   377  				sub.Cache.Del(f.BytesUint64(i))
   378  				atomic.AddUint64(&delIndex, 1)
   379  			}
   380  		}(sub.Index, uint64(handRecords))
   381  	}
   382  
   383  	for {
   384  		select {
   385  		case <-ctx.Done():
   386  			if err := ctx.Err(); err != nil && err != context.Canceled {
   387  				Log.Warn().Msgf("[nats] done handle new data err > %s", err)
   388  			}
   389  			for running {
   390  				time.Sleep(time.Millisecond)
   391  			}
   392  			runHandle()
   393  			return
   394  		case <-time.After(sub.OnceInterval):
   395  			if running {
   396  				continue
   397  			}
   398  			running = true
   399  			Log.Debug().Msgf("[nats] run receive new data > %d/%d", sub.Index, sub.Count)
   400  			runHandle()
   401  			running = false
   402  		}
   403  	}
   404  }
   405  
   406  // Dirname gets the Cache Dirname.
   407  func (sub *SubscriberFastCache) Dirname() string {
   408  	return sub.dirname(sub.Since, sub.Index, sub.Count)
   409  }
   410  
   411  func (sub *SubscriberFastCache) dirname(since *f.TimeStamp, index, count uint64) string {
   412  	return sub.dirnames(since.LocalTimeStampString(true), index, count)
   413  }
   414  
   415  func (sub *SubscriberFastCache) dirnames(since string, index, count uint64) string {
   416  	return fmt.Sprintf("%s.%d.%d", since, index, count)
   417  }
   418  
   419  // Filename gets the Cache Filename.
   420  func (sub *SubscriberFastCache) Filename() string {
   421  	return sub.filename(sub.Since, sub.Index, sub.Count)
   422  }
   423  
   424  func (sub *SubscriberFastCache) filename(since *f.TimeStamp, index, count uint64) string {
   425  	return sub.filenames(since.LocalTimeStampString(true), index, count)
   426  }
   427  
   428  func (sub *SubscriberFastCache) filenames(since string, index, count uint64) string {
   429  	return fmt.Sprintf("%s.%d.%d.json", since, index, count)
   430  }
   431  
   432  // Save the Cache Data of some records not processed.
   433  func (sub *SubscriberFastCache) Save(cacheDir string) {
   434  	if sub.Count == 0 || sub.Hand == nil || sub.Index == sub.Count {
   435  		return
   436  	}
   437  
   438  	saveFastCache(sub.Cache, cacheDir, sub.Dirname(), sub.Filename())
   439  }
   440  
   441  func saveFastCache(cache *fastcache.Cache, cacheDir, dirname, filename string) {
   442  	fileStat := new(fastcache.Stats)
   443  	cache.UpdateStats(fileStat)
   444  	handData, err := f.EncodeJson(fileStat)
   445  	if err != nil {
   446  		Log.Error().Msgf("[nats] save cache stats > %s", err)
   447  	}
   448  
   449  	filePath := filepath.Join(cacheDir, filename)
   450  	err = ioutil.WriteFile(filePath, handData, 0644)
   451  	if err != nil {
   452  		Log.Error().Msgf("[nats] save cache stats > %s", err)
   453  	}
   454  
   455  	dirPath := filepath.Join(cacheDir, dirname)
   456  	if err = cache.SaveToFileConcurrent(dirPath, 0); err != nil {
   457  		Log.Error().Msgf("[nats] save cache data > %s", err)
   458  	} else {
   459  		cache.Reset() // Reset removes all the items from the cache.
   460  	}
   461  }