github.com/angenalZZZ/gofunc@v0.0.0-20210507121333-48ff1be3917b/rpc/nats/subscriber_fast.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"
    19  	"github.com/angenalZZZ/gofunc/f"
    20  	"github.com/nats-io/nats.go"
    21  )
    22  
    23  // SubscriberFast The NatS Subscriber with Fast Concurrent Map Temporary Storage.
    24  type SubscriberFast struct {
    25  	*nats.Conn
    26  	sub          *nats.Subscription
    27  	Subj         string
    28  	Hand         func([][]byte) error
    29  	Cache        f.CiMap
    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  	async        bool
    39  	err          error
    40  }
    41  
    42  // NewSubscriberFast Create a subscriber with cache store for Client Connect.
    43  func NewSubscriberFast(nc *nats.Conn, subject string, cacheDir ...string) *SubscriberFast {
    44  	sub := &SubscriberFast{
    45  		Conn:         nc,
    46  		Subj:         subject,
    47  		Cache:        f.NewCiMap(), // fast Concurrent Map
    48  		Since:        f.TimeFrom(time.Now(), true),
    49  		MsgLimit:     100000000, // pending messages: 100 million
    50  		BytesLimit:   1048576,   // a message's size: 1MB
    51  		OnceAmount:   -1,
    52  		OnceInterval: time.Second,
    53  		async:        true,
    54  	}
    55  	if len(cacheDir) == 1 && cacheDir[0] != "" {
    56  		sub.CacheDir = cacheDir[0]
    57  	} else {
    58  		sub.CacheDir = data.CurrentDir
    59  	}
    60  	return sub
    61  }
    62  
    63  // LimitMessage sets amount for pending messages for this subscription, and a message's bytes.
    64  // Defaults amountPendingMessages: 100 million, anMessageBytes: 1MB
    65  func (sub *SubscriberFast) LimitMessage(amountPendingMessages, anMessageBytes int) {
    66  	sub.MsgLimit, sub.BytesLimit = amountPendingMessages, anMessageBytes
    67  }
    68  
    69  // LimitAmount sets amount allocated at one time, and the processing interval time.
    70  // Defaults onceAmount: -1, onceInterval: time.Second
    71  func (sub *SubscriberFast) LimitAmount(onceAmount int64, onceInterval time.Duration) {
    72  	sub.OnceAmount, sub.OnceInterval = onceAmount, onceInterval
    73  }
    74  
    75  // Run runtime to end your application.
    76  func (sub *SubscriberFast) Run(waitFunc ...func()) {
    77  	ctx, cancel := context.WithCancel(context.Background())
    78  
    79  	// Handle panic.
    80  	defer func() {
    81  		err := recover()
    82  		if err != nil {
    83  			Log.Error().Msgf("[nats] stop receive new data with error.panic > %v", err)
    84  		} else {
    85  			Log.Warn().Msgf("[nats] stop receive new data > %d/%d < %d records not processed", sub.Index, sub.Count, sub.Count-sub.Index)
    86  		}
    87  
    88  		// Unsubscribe will remove interest in the given subject.
    89  		_ = sub.sub.Unsubscribe()
    90  		// Drain connection (Preferred for responders), Close() not needed if this is called.
    91  		_ = sub.Conn.Drain()
    92  
    93  		// Stop handle new data.
    94  		cancel()
    95  		// Save cache.
    96  		sub.Save(sub.CacheDir)
    97  
    98  		// os.Exit(1)
    99  		if err != nil {
   100  			log.Fatal(err)
   101  		}
   102  	}()
   103  
   104  	// Async Subscriber.
   105  	sub.sub, sub.err = sub.Conn.Subscribe(sub.Subj, func(msg *nats.Msg) {
   106  		key, val := atomic.AddUint64(&sub.Count, 1), msg.Data
   107  		sub.Cache.Set(key, val)
   108  	})
   109  	// Set listening.
   110  	SubscribeErrorHandle(sub.sub, sub.async, sub.err)
   111  	if sub.err != nil {
   112  		log.Fatal(sub.err)
   113  	}
   114  
   115  	// Set pending limits.
   116  	SubscribeLimitHandle(sub.sub, sub.MsgLimit, sub.BytesLimit)
   117  
   118  	// Flush connection to server, returns when all messages have been processed.
   119  	FlushAndCheckLastError(sub.Conn)
   120  
   121  	// init handle old data.
   122  	go sub.init(ctx)
   123  
   124  	// run handle new data.
   125  	go sub.hand(ctx)
   126  
   127  	if len(waitFunc) > 0 {
   128  		Log.Info().Msg("[nats] start running and wait to auto exit")
   129  		waitFunc[0]()
   130  		return
   131  	}
   132  
   133  	Log.Info().Msg("[nats] start running and wait to manual exit")
   134  
   135  	// Pass the signals you want to end your application.
   136  	death := f.NewDeath(syscall.SIGINT, syscall.SIGTERM)
   137  	// When you want to block for shutdown signals.
   138  	death.WaitForDeathWithFunc(func() {
   139  		Log.Warn().Msg("[nats] forced to shutdown.")
   140  	})
   141  }
   142  
   143  // init handle old data.
   144  func (sub *SubscriberFast) init(ctx context.Context) {
   145  	if sub.Hand == nil {
   146  		return
   147  	}
   148  
   149  	oldFiles, _ := filepath.Glob(filepath.Join(sub.CacheDir, "*"))
   150  	sort.Strings(oldFiles)
   151  
   152  	cacheDir := sub.CacheDir
   153  	var clearCache = func(cache f.CiMap, index, count int64) {
   154  		for i, c := uint64(index)+1, uint64(count); i <= c; i++ {
   155  			cache.Remove(i)
   156  		}
   157  	}
   158  
   159  	handRecords, onceRecords := 0, atomic.LoadInt64(&sub.OnceAmount)
   160  	for _, oldFile := range oldFiles {
   161  		_, jsonFile := filepath.Split(oldFile)
   162  		if ok, _ := regexp.MatchString(`^\d+\.\d+\.\d+\.json$`, jsonFile); !ok {
   163  			continue
   164  		}
   165  
   166  		dirname := strings.ReplaceAll(jsonFile, ".json", "")
   167  		fileData, err := ioutil.ReadFile(oldFile)
   168  		if err != nil {
   169  			continue
   170  		}
   171  
   172  		cache, err := f.NewCiMapFromJSON(fileData)
   173  		s := strings.Split(dirname, ".")
   174  		if err != nil || len(s) != 3 {
   175  			continue
   176  		}
   177  
   178  		since := s[0]
   179  		index, _ := strconv.ParseInt(s[1], 10, 0)
   180  		count, _ := strconv.ParseInt(s[2], 10, 0)
   181  		indexZero, indexSize := uint64(index)+1, count-index
   182  		if onceRecords > 0 {
   183  			indexSize = onceRecords
   184  		}
   185  
   186  		var handData = make([][]byte, 0, indexSize)
   187  		for i, c, dataIndex := indexZero, uint64(count), int64(0); i <= c; i++ {
   188  			if val, ok := cache.Get(i); ok {
   189  				handData = append(handData, val.([]byte))
   190  				if dataIndex++; dataIndex == onceRecords || i == c {
   191  					// bulk handle
   192  					if err := sub.Hand(handData); err != nil {
   193  						// rollback
   194  						Log.Error().Msgf("[nats] init handle old data > %s > %s", dirname, err)
   195  						if i > indexZero {
   196  							clearCache(cache, int64(indexZero)-1, int64(i))
   197  							dirname1, filename1 := sub.dirnames(since, i-1, c), sub.filenames(since, i-1, c)
   198  							saveFastMap(cache, cacheDir, dirname1, filename1)
   199  							_ = os.Remove(oldFile)
   200  						}
   201  						// reboot init handle old data
   202  						time.Sleep(sub.OnceInterval)
   203  						sub.init(ctx)
   204  						return
   205  					}
   206  					handRecords += len(handData)
   207  					// reset data
   208  					dataIndex = 0
   209  					handData = make([][]byte, 0, indexSize)
   210  					time.Sleep(sub.OnceInterval)
   211  				}
   212  			}
   213  		}
   214  
   215  		_ = os.Remove(oldFile)
   216  	}
   217  
   218  	Log.Info().Msgf("[nats] init handle old data > %d records", handRecords)
   219  	if err := ctx.Err(); err != nil && err != context.Canceled {
   220  		Log.Warn().Msgf("[nats] init handle old data err > %s", err)
   221  	}
   222  }
   223  
   224  // run handle new data.
   225  func (sub *SubscriberFast) hand(ctx context.Context) {
   226  	if sub.Hand == nil {
   227  		return
   228  	}
   229  
   230  	var (
   231  		running  bool
   232  		runCount uint64
   233  		delIndex uint64
   234  	)
   235  
   236  	var runHandle = func() {
   237  		count, index := atomic.LoadUint64(&sub.Count), atomic.LoadUint64(&sub.Index)
   238  		if count <= runCount {
   239  			// reset handle
   240  			if 0 < count && 3 == time.Now().Hour() && index <= delIndex {
   241  				sub.Count, runCount, sub.Index, delIndex = 0, 0, 0, 0
   242  			}
   243  			return
   244  		}
   245  
   246  		handRecords, onceRecords := int64(0), atomic.LoadInt64(&sub.OnceAmount)
   247  		indexSize := int64(count - index)
   248  		if onceRecords > 0 {
   249  			indexSize = onceRecords
   250  		}
   251  
   252  		var handData = make([][]byte, 0, indexSize)
   253  		for dataIndex := int64(0); dataIndex < indexSize && index < count; runCount++ {
   254  			index++ // key equals index
   255  			val, ok := sub.Cache.Get(index)
   256  			if !ok || val == nil {
   257  				val = []byte{}
   258  			}
   259  			handData = append(handData, val.([]byte))
   260  			handRecords++
   261  			if dataIndex++; dataIndex == indexSize || index == count {
   262  				// bulk handle
   263  				if err := sub.Hand(handData); err != nil {
   264  					// rollback
   265  					Log.Error().Msgf("[nats] run handle new data err > %s", err)
   266  					handRecords -= dataIndex
   267  					break
   268  				}
   269  				sub.Index += uint64(dataIndex)
   270  				// reset data
   271  				dataIndex = 0
   272  				handData = make([][]byte, 0, indexSize)
   273  			}
   274  		}
   275  
   276  		if handRecords == 0 {
   277  			return
   278  		}
   279  
   280  		Log.Info().Msgf("[nats] run handle new data > %d/%d < %d records", sub.Index, sub.Count, handRecords)
   281  
   282  		// delete old data
   283  		go func(index, handRecords uint64) {
   284  			for i, n := index-handRecords+1, index; i <= n; i++ {
   285  				sub.Cache.Remove(i)
   286  				atomic.AddUint64(&delIndex, 1)
   287  			}
   288  		}(sub.Index, uint64(handRecords))
   289  	}
   290  
   291  	for {
   292  		select {
   293  		case <-ctx.Done():
   294  			if err := ctx.Err(); err != nil && err != context.Canceled {
   295  				Log.Warn().Msgf("[nats] done handle new data err > %s", err)
   296  			}
   297  			for running {
   298  				time.Sleep(time.Millisecond)
   299  			}
   300  			runHandle()
   301  			return
   302  		case <-time.After(sub.OnceInterval):
   303  			if running {
   304  				continue
   305  			}
   306  			running = true
   307  			Log.Debug().Msgf("[nats] run receive new data > %d/%d", sub.Index, sub.Count)
   308  			runHandle()
   309  			running = false
   310  		}
   311  	}
   312  }
   313  
   314  // Dirname gets the Cache Dirname.
   315  func (sub *SubscriberFast) Dirname() string {
   316  	return sub.dirname(sub.Since, sub.Index, sub.Count)
   317  }
   318  
   319  func (sub *SubscriberFast) dirname(since *f.TimeStamp, index, count uint64) string {
   320  	return sub.dirnames(since.LocalTimeStampString(true), index, count)
   321  }
   322  
   323  func (sub *SubscriberFast) dirnames(since string, index, count uint64) string {
   324  	return fmt.Sprintf("%s.%d.%d", since, index, count)
   325  }
   326  
   327  // Filename gets the Cache Filename.
   328  func (sub *SubscriberFast) Filename() string {
   329  	return sub.filename(sub.Since, sub.Index, sub.Count)
   330  }
   331  
   332  func (sub *SubscriberFast) filename(since *f.TimeStamp, index, count uint64) string {
   333  	return sub.filenames(since.LocalTimeStampString(true), index, count)
   334  }
   335  
   336  func (sub *SubscriberFast) filenames(since string, index, count uint64) string {
   337  	return fmt.Sprintf("%s.%d.%d.json", since, index, count)
   338  }
   339  
   340  // Save the Cache Data of some records not processed.
   341  func (sub *SubscriberFast) Save(cacheDir string) {
   342  	if sub.Count == 0 || sub.Hand == nil || sub.Index == sub.Count {
   343  		return
   344  	}
   345  
   346  	saveFastMap(sub.Cache, cacheDir, sub.Dirname(), sub.Filename())
   347  }
   348  
   349  func saveFastMap(cache f.CiMap, cacheDir, dirname, filename string) {
   350  	handData, err := cache.JSON()
   351  	if err != nil {
   352  		Log.Error().Msgf("[nats] save cache stats > %s", err)
   353  	}
   354  
   355  	filePath := filepath.Join(cacheDir, filename)
   356  	err = ioutil.WriteFile(filePath, handData, 0644)
   357  	if err != nil {
   358  		Log.Error().Msgf("[nats] save cache stats > %s", err)
   359  	}
   360  }