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

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